@@ -24,11 +24,18 @@ const ROUTINE_NAME_PATTERN = new RegExp(`^${IDENTIFIER_START}${IDENTIFIER_BODY}*
2424const CLASS_METHOD_NAME_PATTERN = new RegExp ( `^${ IDENTIFIER_START } ${ IDENTIFIER_BODY } *$` ) ;
2525const ROUTINE_LABEL_NAME_PATTERN = new RegExp ( `^[A-Za-z0-9_%][A-Za-z0-9_%]*$` ) ;
2626
27+ const JUMP_QP_CONTEXT_KEY = "vscode-objectscript.ccs.jumpToTagQuickPickActive" ;
28+ const INSERT_SELECTION_COMMAND_ID = "vscode-objectscript.ccs.jumpToTagOffsetCrossEntity.insertSelection" ;
29+ const QUICK_PICK_OVERLAY_LINE_PADDING = 6 ;
30+ const EXTRA_LINES_BELOW_QP = 2 ;
31+
2732type EntityKind = "class" | "routine" | "unknown" ;
2833
2934interface LocalNameInfo {
3035 readonly line : number ;
3136 readonly originalName : string ;
37+ readonly selectionRange ?: vscode . Range ;
38+ readonly blockRange ?: vscode . Range ;
3239}
3340
3441type LocalNamesMap = Map < string , LocalNameInfo > ;
@@ -77,7 +84,14 @@ export async function jumpToTagAndOffsetCrossEntity(): Promise<void> {
7784 let pendingValidationError : string | undefined ;
7885
7986 while ( true ) {
80- 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+ ) ;
8195 if ( ! parsed ) return ;
8296
8397 previousValue = parsed . input ;
@@ -95,27 +109,130 @@ async function promptWithQuickPick(
95109 previousValue : string | undefined ,
96110 initialValidationError : string | undefined ,
97111 localNames : LocalNamesMap ,
98- docCtx : DocContext
112+ docCtx : DocContext ,
113+ document : vscode . TextDocument ,
114+ editor : vscode . TextEditor
99115) : 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+
100122 const qp = vscode . window . createQuickPick < vscode . QuickPickItem > ( ) ;
101- qp . title = "Consistem — Ir para Nome + Offset ^ Item" ;
123+ qp . title = "Navegar para Definição (+ Offset ^Item) " ;
102124 qp . placeholder = docCtx . placeholder ;
103125 qp . ignoreFocusOut = true ;
104126 qp . matchOnDescription = true ;
105127 qp . matchOnDetail = true ;
106128 qp . canSelectMany = false ;
107129
130+ const disposables : vscode . Disposable [ ] = [ ] ;
131+ let cleanedUp = false ;
132+
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+
201+ const cleanup = ( ) => {
202+ if ( cleanedUp ) return ;
203+ cleanedUp = true ;
204+ while ( disposables . length ) {
205+ const d = disposables . pop ( ) ;
206+ try {
207+ d ?. dispose ( ) ;
208+ } catch {
209+ // Ignore dispose errors.
210+ }
211+ }
212+ clearHighlight ( ) ;
213+ void vscode . commands . executeCommand ( "setContext" , JUMP_QP_CONTEXT_KEY , false ) ;
214+ } ;
215+
216+ void vscode . commands . executeCommand ( "setContext" , JUMP_QP_CONTEXT_KEY , true ) ;
217+
108218 let lastParse : ParseSuccess | undefined ;
109219 let lastValidatedValue : string | undefined ;
110220 let currentValidationId = 0 ;
111221 let lastValidationPromise : Promise < void > | undefined ;
112222
113223 qp . value = previousValue ?? "" ;
114224
115- const localItems : vscode . QuickPickItem [ ] = buildLocalItems ( localNames ) ;
225+ const { items : localItems , tipItem } = buildLocalItems ( localNames ) ;
116226 const setItems = ( ) => ( qp . items = localItems ) ;
117227 setItems ( ) ;
118228
229+ try {
230+ ( qp as any ) . activeItems = [ tipItem ] ;
231+ ( qp as any ) . selectedItems = [ ] ;
232+ } catch {
233+ /* ignore */
234+ }
235+
119236 if ( initialValidationError ) {
120237 vscode . window . showErrorMessage ( initialValidationError ) ;
121238 } else if ( qp . value . trim ( ) !== "" ) {
@@ -152,10 +269,51 @@ async function promptWithQuickPick(
152269 return p ;
153270 }
154271
272+ const applySelectedItemToValue = ( { revalidate } : { revalidate ?: boolean } = { } ) : boolean => {
273+ const picked = qp . selectedItems [ 0 ] ?? qp . activeItems [ 0 ] ;
274+ if ( ! picked ) return false ;
275+
276+ if ( ( picked as any ) . __isTipItem ) return false ;
277+
278+ const trimmed = qp . value . trim ( ) ;
279+ const normalized = replaceNameInExpression ( trimmed , picked . label ) ;
280+ if ( normalized === qp . value ) return false ;
281+
282+ qp . value = normalized ;
283+
284+ try {
285+ ( qp as any ) . selectedItems = [ ] ;
286+ } catch {
287+ // Ignore errors from manipulating QuickPick internals.
288+ }
289+
290+ if ( revalidate && qp . value . trim ( ) !== "" ) {
291+ void runValidation ( qp . value , localNames , docCtx , false ) ;
292+ }
293+
294+ return true ;
295+ } ;
296+
297+ const insertSelectionDisposable = vscode . commands . registerCommand ( INSERT_SELECTION_COMMAND_ID , ( ) => {
298+ applySelectedItemToValue ( { revalidate : true } ) ;
299+ } ) ;
300+ disposables . push ( insertSelectionDisposable ) ;
301+
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+
155312 qp . onDidChangeValue ( ( value ) => {
156313 if ( value . trim ( ) === "" ) {
157314 lastParse = undefined ;
158315 lastValidatedValue = undefined ;
316+ clearHighlight ( ) ;
159317 return ;
160318 }
161319
@@ -164,24 +322,9 @@ async function promptWithQuickPick(
164322
165323 const accepted = new Promise < ParseSuccess | undefined > ( ( resolve ) => {
166324 qp . onDidAccept ( async ( ) => {
167- const trimmed = qp . value . trim ( ) ;
168-
169- if ( qp . selectedItems . length ) {
170- const picked = qp . selectedItems [ 0 ] ;
171- const normalized = replaceNameInExpression ( trimmed , picked . label ) ;
172- if ( normalized !== trimmed ) {
173- qp . value = normalized ;
174-
175- try {
176- ( qp as any ) . selectedItems = [ ] ;
177- } catch {
178- // Ignore errors from manipulating QuickPick internals.
179- }
325+ applySelectedItemToValue ( ) ;
180326
181- if ( qp . value . trim ( ) !== "" ) void runValidation ( qp . value , localNames , docCtx , false ) ;
182- return ;
183- }
184- }
327+ const trimmed = qp . value . trim ( ) ;
185328
186329 if ( trimmed === "" ) {
187330 vscode . window . showErrorMessage ( ERR_NAME_REQUIRED ) ;
@@ -200,39 +343,69 @@ async function promptWithQuickPick(
200343 if ( ! lastParse ) return ;
201344
202345 resolve ( lastParse ) ;
203- qp . hide ( ) ;
346+ wasAccepted = true ;
347+ cleanup ( ) ;
204348 qp . dispose ( ) ;
205349 } ) ;
206350
207351 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+ }
208364 resolve ( undefined ) ;
209- qp . dispose ( ) ;
365+ cleanup ( ) ;
210366 } ) ;
211367 } ) ;
212368
213369 qp . show ( ) ;
214370 return accepted ;
215371}
216372
217- function buildLocalItems ( localNames : LocalNamesMap ) : vscode . QuickPickItem [ ] {
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 ;
385+
218386 if ( ! localNames . size ) {
219- return [
220- {
221- label : "Nenhum nome local encontrado" ,
222- description : "—" ,
223- detail : "Defina métodos/labels no arquivo atual para listá-los aqui." ,
224- alwaysShow : true ,
225- } ,
226- ] ;
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+ } ;
227399 }
228400
229- return [ ...localNames . values ( ) ]
401+ const items = [ ...localNames . values ( ) ]
230402 . sort ( ( a , b ) => a . line - b . line || a . originalName . localeCompare ( b . originalName ) )
231403 . map ( ( info ) => ( {
232404 label : info . originalName ,
233405 description : "definição local" ,
234- detail : `linha ${ info . line + 1 } ` ,
235406 } ) ) ;
407+
408+ return { tipItem, items : [ tipItem , ...items ] } ;
236409}
237410
238411/** Replaces only the "name" portion in the expression, preserving +offset and ^item. */
@@ -340,7 +513,12 @@ async function collectLocalNames(document: vscode.TextDocument): Promise<LocalNa
340513 const line = symbol . selectionRange ?. start . line ?? symbol . range . start . line ;
341514 const key = symbol . name . toLowerCase ( ) ;
342515 if ( ! map . has ( key ) ) {
343- 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+ } ) ;
344522 }
345523 }
346524 if ( symbol . children ?. length ) pending . push ( ...symbol . children ) ;
0 commit comments