@@ -26,12 +26,16 @@ const ROUTINE_LABEL_NAME_PATTERN = new RegExp(`^[A-Za-z0-9_%][A-Za-z0-9_%]*$`);
2626
2727const JUMP_QP_CONTEXT_KEY = "vscode-objectscript.ccs.jumpToTagQuickPickActive" ;
2828const INSERT_SELECTION_COMMAND_ID = "vscode-objectscript.ccs.jumpToTagOffsetCrossEntity.insertSelection" ;
29+ const QUICK_PICK_OVERLAY_LINE_PADDING = 6 ;
30+ const EXTRA_LINES_BELOW_QP = 2 ;
2931
3032type EntityKind = "class" | "routine" | "unknown" ;
3133
3234interface LocalNameInfo {
3335 readonly line : number ;
3436 readonly originalName : string ;
37+ readonly selectionRange ?: vscode . Range ;
38+ readonly blockRange ?: vscode . Range ;
3539}
3640
3741type LocalNamesMap = Map < string , LocalNameInfo > ;
@@ -80,7 +84,14 @@ export async function jumpToTagAndOffsetCrossEntity(): Promise<void> {
8084 let pendingValidationError : string | undefined ;
8185
8286 while ( true ) {
83- const parsed = await promptWithQuickPick ( previousValue , pendingValidationError , localNames , docCtx ) ;
87+ const parsed = await promptWithQuickPick (
88+ previousValue ,
89+ pendingValidationError ,
90+ localNames ,
91+ docCtx ,
92+ document ,
93+ editor
94+ ) ;
8495 if ( ! parsed ) return ;
8596
8697 previousValue = parsed . input ;
@@ -98,10 +109,18 @@ async function promptWithQuickPick(
98109 previousValue : string | undefined ,
99110 initialValidationError : string | undefined ,
100111 localNames : LocalNamesMap ,
101- docCtx : DocContext
112+ docCtx : DocContext ,
113+ document : vscode . TextDocument ,
114+ editor : vscode . TextEditor
102115) : Promise < ParseSuccess | undefined > {
116+ // Remember where the user was before opening the QuickPick,
117+ // so we can restore on ESC (cancel).
118+ const originalSelection = editor . selection ;
119+ const originalVisible = editor . visibleRanges ?. [ 0 ] ;
120+ let wasAccepted = false ;
121+
103122 const qp = vscode . window . createQuickPick < vscode . QuickPickItem > ( ) ;
104- qp . title = "Consistem — Ir para Definição + Offset ^ Item" ;
123+ qp . title = "Navegar para Definição (+ Offset ^Item) " ;
105124 qp . placeholder = docCtx . placeholder ;
106125 qp . ignoreFocusOut = true ;
107126 qp . matchOnDescription = true ;
@@ -111,6 +130,74 @@ async function promptWithQuickPick(
111130 const disposables : vscode . Disposable [ ] = [ ] ;
112131 let cleanedUp = false ;
113132
133+ const blockHighlightDecoration = vscode . window . createTextEditorDecorationType ( {
134+ backgroundColor : new vscode . ThemeColor ( "editor.rangeHighlightBackground" ) ,
135+ isWholeLine : true ,
136+ } ) ;
137+ disposables . push ( blockHighlightDecoration ) ;
138+
139+ const highlightDecoration = vscode . window . createTextEditorDecorationType ( {
140+ borderColor : new vscode . ThemeColor ( "editor.selectionHighlightBorder" ) ,
141+ borderStyle : "solid" ,
142+ borderWidth : "1px" ,
143+ } ) ;
144+ disposables . push ( highlightDecoration ) ;
145+
146+ let lastHighlightedRange : vscode . Range | undefined ;
147+ let lastHighlightedBlockRange : vscode . Range | undefined ;
148+
149+ const clearHighlight = ( ) => {
150+ if ( ! lastHighlightedRange && ! lastHighlightedBlockRange ) return ;
151+ lastHighlightedRange = undefined ;
152+ lastHighlightedBlockRange = undefined ;
153+ editor . setDecorations ( highlightDecoration , [ ] ) ;
154+ editor . setDecorations ( blockHighlightDecoration , [ ] ) ;
155+ } ;
156+
157+ const highlightInfo = ( info ?: LocalNameInfo ) => {
158+ if ( ! info ) {
159+ clearHighlight ( ) ;
160+ return ;
161+ }
162+
163+ const range = info . selectionRange ?? document . lineAt ( info . line ) . range ;
164+ const blockRange = info . blockRange ?? range ;
165+ lastHighlightedRange = range ;
166+ lastHighlightedBlockRange = blockRange ;
167+ editor . setDecorations ( blockHighlightDecoration , [ blockRange ] ) ;
168+ editor . setDecorations ( highlightDecoration , [ range ] ) ;
169+
170+ // Keep highlighted block below the QuickPick overlay.
171+ // We derive a dynamic padding from the current visible height,
172+ // falling back to the fixed constant when needed.
173+ const visible = editor . visibleRanges ?. [ 0 ] ;
174+ const visibleHeight = visible
175+ ? Math . max ( 0 , visible . end . line - visible . start . line )
176+ : QUICK_PICK_OVERLAY_LINE_PADDING * 3 ;
177+ const dynamicGap = Math . floor ( visibleHeight * 0.35 ) ;
178+ const gap = Math . max ( QUICK_PICK_OVERLAY_LINE_PADDING , dynamicGap ) + EXTRA_LINES_BELOW_QP ;
179+
180+ const revealStartLine = Math . max ( blockRange . start . line - gap , 0 ) ;
181+ const revealRangeStart = new vscode . Position ( revealStartLine , 0 ) ;
182+ const revealRange = new vscode . Range ( revealRangeStart , blockRange . end ) ;
183+ editor . revealRange ( revealRange , vscode . TextEditorRevealType . AtTop ) ;
184+ } ;
185+
186+ const updateHighlightFromItem = ( item : vscode . QuickPickItem | undefined ) => {
187+ if ( ! item ) {
188+ clearHighlight ( ) ;
189+ return ;
190+ }
191+
192+ // Ignore tip item (first blank row)
193+ if ( ( item as any ) . __isTipItem ) {
194+ clearHighlight ( ) ;
195+ return ;
196+ }
197+ const info = localNames . get ( item . label . toLowerCase ( ) ) ;
198+ highlightInfo ( info ) ;
199+ } ;
200+
114201 const cleanup = ( ) => {
115202 if ( cleanedUp ) return ;
116203 cleanedUp = true ;
@@ -122,6 +209,7 @@ async function promptWithQuickPick(
122209 // Ignore dispose errors.
123210 }
124211 }
212+ clearHighlight ( ) ;
125213 void vscode . commands . executeCommand ( "setContext" , JUMP_QP_CONTEXT_KEY , false ) ;
126214 } ;
127215
@@ -134,10 +222,17 @@ async function promptWithQuickPick(
134222
135223 qp . value = previousValue ?? "" ;
136224
137- const localItems : vscode . QuickPickItem [ ] = buildLocalItems ( localNames ) ;
225+ const { items : localItems , tipItem } = buildLocalItems ( localNames ) ;
138226 const setItems = ( ) => ( qp . items = localItems ) ;
139227 setItems ( ) ;
140228
229+ try {
230+ ( qp as any ) . activeItems = [ tipItem ] ;
231+ ( qp as any ) . selectedItems = [ ] ;
232+ } catch {
233+ /* ignore */
234+ }
235+
141236 if ( initialValidationError ) {
142237 vscode . window . showErrorMessage ( initialValidationError ) ;
143238 } else if ( qp . value . trim ( ) !== "" ) {
@@ -178,6 +273,8 @@ async function promptWithQuickPick(
178273 const picked = qp . selectedItems [ 0 ] ?? qp . activeItems [ 0 ] ;
179274 if ( ! picked ) return false ;
180275
276+ if ( ( picked as any ) . __isTipItem ) return false ;
277+
181278 const trimmed = qp . value . trim ( ) ;
182279 const normalized = replaceNameInExpression ( trimmed , picked . label ) ;
183280 if ( normalized === qp . value ) return false ;
@@ -202,10 +299,21 @@ async function promptWithQuickPick(
202299 } ) ;
203300 disposables . push ( insertSelectionDisposable ) ;
204301
302+ const changeActiveDisposable = qp . onDidChangeActive ( ( items ) => {
303+ updateHighlightFromItem ( items [ 0 ] ) ;
304+ } ) ;
305+ disposables . push ( changeActiveDisposable ) ;
306+
307+ const changeSelectionDisposable = qp . onDidChangeSelection ( ( items ) => {
308+ updateHighlightFromItem ( items [ 0 ] ) ;
309+ } ) ;
310+ disposables . push ( changeSelectionDisposable ) ;
311+
205312 qp . onDidChangeValue ( ( value ) => {
206313 if ( value . trim ( ) === "" ) {
207314 lastParse = undefined ;
208315 lastValidatedValue = undefined ;
316+ clearHighlight ( ) ;
209317 return ;
210318 }
211319
@@ -235,11 +343,24 @@ async function promptWithQuickPick(
235343 if ( ! lastParse ) return ;
236344
237345 resolve ( lastParse ) ;
346+ wasAccepted = true ;
238347 cleanup ( ) ;
239348 qp . dispose ( ) ;
240349 } ) ;
241350
242351 qp . onDidHide ( ( ) => {
352+ // If user cancelled (ESC), restore cursor and viewport.
353+ if ( ! wasAccepted ) {
354+ try {
355+ editor . selection = originalSelection ;
356+ if ( originalVisible ) {
357+ // Use Default so VS Code restores without forcing center/top.
358+ editor . revealRange ( originalVisible , vscode . TextEditorRevealType . Default ) ;
359+ }
360+ } catch {
361+ /* ignore */
362+ }
363+ }
243364 resolve ( undefined ) ;
244365 cleanup ( ) ;
245366 } ) ;
@@ -249,39 +370,42 @@ async function promptWithQuickPick(
249370 return accepted ;
250371}
251372
252- /**
253- * Adds a non-selectable tip at the top of the QuickPick using a separator.
254- * This avoids interfering with selection while informing the shortcuts.
255- */
256- function buildLocalItems ( localNames : LocalNamesMap ) : vscode . QuickPickItem [ ] {
257- // Non-selectable header (separator) with the tip
258- const infoSeparator = {
259- label : "Tab ↹ Inserir • Enter ↩ Navegar" ,
260- kind : vscode . QuickPickItemKind . Separator ,
261- } as unknown as vscode . QuickPickItem ; // keep type compatible for qp.items
373+ function buildLocalItems ( localNames : LocalNamesMap ) : {
374+ items : vscode . QuickPickItem [ ] ;
375+ tipItem : vscode . QuickPickItem ;
376+ } {
377+ const tipItem : vscode . QuickPickItem = {
378+ label : "" ,
379+ description : "Tab ↹ Inserir • Enter ↩ Navegar" ,
380+ detail : "" ,
381+ alwaysShow : true ,
382+ } as vscode . QuickPickItem ;
383+
384+ ( tipItem as any ) . __isTipItem = true ;
262385
263386 if ( ! localNames . size ) {
264- return [
265- infoSeparator ,
266- {
267- label : "Nenhum nome local encontrado" ,
268- description : "—" ,
269- detail : "Defina métodos/labels no arquivo atual para listá-los aqui." ,
270- alwaysShow : true ,
271- } ,
272- ] ;
387+ return {
388+ tipItem,
389+ items : [
390+ tipItem ,
391+ {
392+ label : "Nenhum nome local encontrado" ,
393+ description : "—" ,
394+ detail : "Defina métodos/labels no arquivo atual para listá-los aqui." ,
395+ alwaysShow : true ,
396+ } ,
397+ ] ,
398+ } ;
273399 }
274400
275401 const items = [ ...localNames . values ( ) ]
276402 . sort ( ( a , b ) => a . line - b . line || a . originalName . localeCompare ( b . originalName ) )
277403 . map ( ( info ) => ( {
278404 label : info . originalName ,
279405 description : "definição local" ,
280- detail : `linha ${ info . line + 1 } ` ,
281406 } ) ) ;
282407
283- // Keep the informative separator on top.
284- return [ infoSeparator , ...items ] ;
408+ return { tipItem, items : [ tipItem , ...items ] } ;
285409}
286410
287411/** Replaces only the "name" portion in the expression, preserving +offset and ^item. */
@@ -389,7 +513,12 @@ async function collectLocalNames(document: vscode.TextDocument): Promise<LocalNa
389513 const line = symbol . selectionRange ?. start . line ?? symbol . range . start . line ;
390514 const key = symbol . name . toLowerCase ( ) ;
391515 if ( ! map . has ( key ) ) {
392- map . set ( key , { line, originalName : symbol . name } ) ;
516+ map . set ( key , {
517+ line,
518+ originalName : symbol . name ,
519+ selectionRange : symbol . selectionRange ?? symbol . range ,
520+ blockRange : symbol . range ,
521+ } ) ;
393522 }
394523 }
395524 if ( symbol . children ?. length ) pending . push ( ...symbol . children ) ;
0 commit comments