@@ -25,7 +25,10 @@ import {
2525 TopDownTreeIterator ,
2626 ExpandableTreeNode ,
2727} from '@theia/core/lib/browser' ;
28- import { Disposable , DisposableCollection , PreferenceProviderDataChanges , PreferenceProviderProvider , PreferenceSchemaService , PreferenceService , unreachable } from '@theia/core' ;
28+ import {
29+ Disposable , DisposableCollection , nls , PreferenceProviderDataChanges ,
30+ PreferenceProviderProvider , PreferenceSchemaService , PreferenceService , unreachable
31+ } from '@theia/core' ;
2932import { BaseWidget , DEFAULT_SCROLL_OPTIONS } from '@theia/core/lib/browser/widgets/widget' ;
3033import { PreferenceTreeModel , PreferenceFilterChangeEvent , PreferenceFilterChangeSource } from '../preference-tree-model' ;
3134import { PreferenceNodeRendererFactory , GeneralPreferenceNodeRenderer } from './components/preference-node-renderer' ;
@@ -41,7 +44,7 @@ export interface PreferencesEditorState {
4144@injectable ( )
4245export class PreferencesEditorWidget extends BaseWidget implements StatefulWidget {
4346 static readonly ID = 'settings.editor' ;
44- static readonly LABEL = 'Settings Editor' ;
47+ static readonly LABEL = nls . localizeByDefault ( 'Settings Editor' ) ;
4548
4649 override scrollOptions = DEFAULT_SCROLL_OPTIONS ;
4750
@@ -56,6 +59,11 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
5659 protected lastUserSelection = '' ;
5760 protected isAtScrollTop = true ;
5861 protected firstVisibleChildID = '' ;
62+ /**
63+ * Tracks the node ID of the preference row the user last focused.
64+ * Used to scroll back to that preference when the search filter is cleared.
65+ */
66+ protected lastFocusedRendererNodeId = '' ;
5967 protected renderers = new Map < string , GeneralPreferenceNodeRenderer > ( ) ;
6068 protected preferenceDataKeys = new Map < string , string > ( ) ;
6169 // The commonly used section will duplicate preference ID's, so we'll keep a separate list of them.
@@ -95,10 +103,20 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
95103 innerWrapper . classList . add ( 'settings-main-scroll-container' ) ;
96104 this . scrollContainer = innerWrapper ;
97105 innerWrapper . addEventListener ( 'scroll' , this . onScroll , { passive : true } ) ;
106+ innerWrapper . addEventListener ( 'focusin' , e => {
107+ const target = e . target ;
108+ if ( target instanceof HTMLElement ) {
109+ const prefRow = target . closest ( '.single-pref' ) ;
110+ const nodeId = prefRow ?. getAttribute ( 'data-node-id' ) ;
111+ if ( nodeId ) {
112+ this . lastFocusedRendererNodeId = nodeId ;
113+ }
114+ }
115+ } ) ;
98116 this . node . appendChild ( innerWrapper ) ;
99117 const noLeavesMessage = document . createElement ( 'div' ) ;
100118 noLeavesMessage . classList . add ( 'settings-no-results-announcement' ) ;
101- noLeavesMessage . textContent = 'That search query has returned no results.' ;
119+ noLeavesMessage . textContent = nls . localizeByDefault ( 'No Settings Found' ) ;
102120 this . node . appendChild ( noLeavesMessage ) ;
103121 }
104122
@@ -114,20 +132,50 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
114132
115133 protected handleDisplayChange ( e : PreferenceFilterChangeEvent ) : void {
116134 const { isFiltered } = this . model ;
117- const currentFirstVisible = this . firstVisibleChildID ;
118135 const leavesAreVisible = this . areLeavesVisible ( ) ;
136+ // Capture scroll target before handlers modify visibility / DOM layout.
137+ const scrollTarget = this . getScrollTarget ( e . source ) ;
138+
119139 if ( e . source === PreferenceFilterChangeSource . Search ) {
120140 this . handleSearchChange ( isFiltered , leavesAreVisible ) ;
121141 } else if ( e . source === PreferenceFilterChangeSource . Scope ) {
122142 this . handleScopeChange ( isFiltered ) ;
123- this . showInTree ( currentFirstVisible ) ;
124143 } else if ( e . source === PreferenceFilterChangeSource . Schema ) {
125144 this . handleSchemaChange ( isFiltered ) ;
126- this . showInTree ( currentFirstVisible ) ;
127145 } else {
128146 unreachable ( e . source , 'Not all PreferenceFilterChangeSource enum variants handled.' ) ;
129147 }
130- this . resetScroll ( currentFirstVisible , e . source === PreferenceFilterChangeSource . Search ) ;
148+
149+ if ( scrollTarget ) {
150+ this . showInTree ( scrollTarget ) ;
151+ }
152+ this . resetScroll ( scrollTarget ) ;
153+
154+ if ( e . source === PreferenceFilterChangeSource . Search ) {
155+ // Reset focus if search context changes.
156+ this . lastFocusedRendererNodeId = '' ;
157+ }
158+ }
159+
160+ protected getScrollTarget ( source : PreferenceFilterChangeSource ) : string | undefined {
161+ if ( source !== PreferenceFilterChangeSource . Search ) {
162+ return this . firstVisibleChildID ;
163+ }
164+ if ( ! this . model . isFiltered && this . lastFocusedRendererNodeId && this . isRendererInViewport ( this . lastFocusedRendererNodeId ) ) {
165+ return this . lastFocusedRendererNodeId ;
166+ }
167+ return undefined ;
168+ }
169+
170+ protected isRendererInViewport ( nodeId : string ) : boolean {
171+ const { id, collection } = this . analyzeIDAndGetRendererGroup ( nodeId ) ;
172+ const renderer = collection . get ( id ) ;
173+ if ( ! renderer ?. visible ) { return false ; }
174+ const scrollTop = this . scrollContainer . scrollTop ;
175+ const viewportBottom = scrollTop + this . scrollContainer . clientHeight ;
176+ const elementTop = renderer . node . offsetTop ;
177+ const elementBottom = elementTop + renderer . node . offsetHeight ;
178+ return elementTop < viewportBottom && elementBottom > scrollTop ;
131179 }
132180
133181 protected handleRegistryChange ( ) : void {
@@ -232,33 +280,31 @@ export class PreferencesEditorWidget extends BaseWidget implements StatefulWidge
232280 }
233281 }
234282
235- protected resetScroll ( nodeIDToScrollTo ?: string , filterWasCleared : boolean = false ) : void {
283+ protected resetScroll ( nodeIDToScrollTo ?: string ) : void {
236284 if ( this . scrollBar ) { // Absent on widget creation
237- this . doResetScroll ( nodeIDToScrollTo , filterWasCleared ) ;
285+ this . doResetScroll ( nodeIDToScrollTo ) ;
238286 } else {
239287 const interval = setInterval ( ( ) => {
240288 if ( this . scrollBar ) {
241289 clearInterval ( interval ) ;
242- this . doResetScroll ( nodeIDToScrollTo , filterWasCleared ) ;
290+ this . doResetScroll ( nodeIDToScrollTo ) ;
243291 }
244292 } , 500 ) ;
245293 }
246294 }
247295
248- protected doResetScroll ( nodeIDToScrollTo ?: string , filterWasModified : boolean = false ) : void {
296+ protected doResetScroll ( nodeIDToScrollTo ?: string ) : void {
249297 requestAnimationFrame ( ( ) => {
250298 this . scrollBar ?. update ( ) ;
251- if ( filterWasModified ) {
252- this . scrollContainer . scrollTop = 0 ;
253- } else if ( nodeIDToScrollTo ) {
299+ if ( nodeIDToScrollTo ) {
254300 const { id, collection } = this . analyzeIDAndGetRendererGroup ( nodeIDToScrollTo ) ;
255301 const renderer = collection . get ( id ) ;
256302 if ( renderer ?. visible ) {
257303 this . scrollContainer . scrollTo ( 0 , renderer . node . offsetTop ) ;
258304 return ;
259305 }
260306 }
261-
307+ this . scrollContainer . scrollTop = 0 ;
262308 } ) ;
263309 } ;
264310
0 commit comments