@@ -20,6 +20,7 @@ import {
2020 toggleLineComments ,
2121} from './formatting' ;
2222import { computeDiagnostics } from './analysis' ;
23+ import { buildOutline , OutlineNode } from './outline' ;
2324import { findFileReferences } from './links' ;
2425import { parsePathsAliases , resolvePathAlias , prtCandidatePaths } from './paths' ;
2526import { DEFAULT_DIAGNOSTICS_EXCLUDED_KEYWORDS } from './diagnostics-exclusions' ;
@@ -729,6 +730,81 @@ class OpmFlowFoldingRangeProvider implements vscode.FoldingRangeProvider {
729730 }
730731}
731732
733+ // ---------------------------------------------------------------------------
734+ // Outline tree view — section -> keyword navigation
735+ // ---------------------------------------------------------------------------
736+
737+ class OpmFlowOutlineProvider implements vscode . TreeDataProvider < OutlineNode > {
738+ private readonly _onDidChangeTreeData = new vscode . EventEmitter < void > ( ) ;
739+ readonly onDidChangeTreeData = this . _onDidChangeTreeData . event ;
740+
741+ private roots : OutlineNode [ ] = [ ] ;
742+ /** Source document of the current outline, for the reveal command. */
743+ private docUri ?: vscode . Uri ;
744+
745+ constructor ( private readonly index : KeywordIndex ) { }
746+
747+ /** Rebuild the outline from `doc` (or clear it for non-opm-flow docs). */
748+ refresh ( doc ?: vscode . TextDocument ) : void {
749+ if ( doc ?. languageId === 'opm-flow' ) {
750+ this . roots = buildOutline ( doc . getText ( ) . split ( / \r ? \n / ) ) ;
751+ this . docUri = doc . uri ;
752+ } else {
753+ this . roots = [ ] ;
754+ this . docUri = undefined ;
755+ }
756+ this . _onDidChangeTreeData . fire ( ) ;
757+ }
758+
759+ getChildren ( node ?: OutlineNode ) : OutlineNode [ ] {
760+ return node ? node . children : this . roots ;
761+ }
762+
763+ /** Required for `TreeView.reveal` to locate a node. Sections are roots;
764+ * a keyword's parent is the section that contains it (undefined for
765+ * pre-section keywords attached directly to the root). */
766+ getParent ( node : OutlineNode ) : OutlineNode | undefined {
767+ if ( node . kind === 'section' ) return undefined ;
768+ return this . roots . find ( s => s . children . includes ( node ) ) ;
769+ }
770+
771+ getTreeItem ( node : OutlineNode ) : vscode . TreeItem {
772+ const item = new vscode . TreeItem (
773+ node . name ,
774+ node . kind === 'section'
775+ ? vscode . TreeItemCollapsibleState . Expanded
776+ : vscode . TreeItemCollapsibleState . None ,
777+ ) ;
778+ item . iconPath = new vscode . ThemeIcon (
779+ node . kind === 'section' ? 'symbol-namespace' : 'symbol-keyword' ,
780+ ) ;
781+ const summary = resolveKeyword ( this . index , node . name ) ?. summary ;
782+ if ( summary ) item . tooltip = summary ;
783+ if ( node . kind === 'keyword' && this . docUri ) {
784+ item . command = {
785+ command : 'opm-flow.revealKeyword' ,
786+ title : 'Go to keyword' ,
787+ arguments : [ this . docUri , node . line ] ,
788+ } ;
789+ }
790+ return item ;
791+ }
792+
793+ /** Find the deepest node whose declaration line is at or before `line`. */
794+ nodeAtLine ( line : number ) : OutlineNode | undefined {
795+ let match : OutlineNode | undefined ;
796+ for ( const section of this . roots ) {
797+ if ( section . line > line ) break ;
798+ match = section ;
799+ for ( const kw of section . children ) {
800+ if ( kw . line > line ) break ;
801+ match = kw ;
802+ }
803+ }
804+ return match ;
805+ }
806+ }
807+
732808// ---------------------------------------------------------------------------
733809// File-reference link provider — INCLUDE / IMPORT / RESTART / GDFILE
734810// ---------------------------------------------------------------------------
@@ -1258,6 +1334,57 @@ export function activate(context: vscode.ExtensionContext): void {
12581334 new OpmFlowFoldingRangeProvider ( )
12591335 ) ;
12601336
1337+ // --- Outline tree view: section -> keyword navigation ---
1338+ const outlineProvider = new OpmFlowOutlineProvider ( index ) ;
1339+ const outlineView = vscode . window . createTreeView ( 'opm-flow.outlineView' , {
1340+ treeDataProvider : outlineProvider ,
1341+ } ) ;
1342+ outlineProvider . refresh ( vscode . window . activeTextEditor ?. document ) ;
1343+
1344+ const revealKeywordCommand = vscode . commands . registerCommand (
1345+ 'opm-flow.revealKeyword' ,
1346+ async ( uri : vscode . Uri , line : number ) => {
1347+ const editor = await vscode . window . showTextDocument ( uri ) ;
1348+ const pos = new vscode . Position ( line , 0 ) ;
1349+ editor . selection = new vscode . Selection ( pos , pos ) ;
1350+ editor . revealRange (
1351+ new vscode . Range ( pos , pos ) ,
1352+ vscode . TextEditorRevealType . InCenter ,
1353+ ) ;
1354+ } ,
1355+ ) ;
1356+
1357+ const refreshOutline = debounce ( ( doc : vscode . TextDocument ) => {
1358+ outlineProvider . refresh ( doc ) ;
1359+ } , 250 ) ;
1360+
1361+ // Keep the tree's selection in sync with the cursor's active keyword.
1362+ let lastRevealedLine = - 1 ;
1363+ const syncOutlineSelection = ( editor : vscode . TextEditor ) : void => {
1364+ if ( editor . document . languageId !== 'opm-flow' || ! outlineView . visible ) return ;
1365+ const node = outlineProvider . nodeAtLine ( editor . selection . active . line ) ;
1366+ if ( ! node || node . line === lastRevealedLine ) return ;
1367+ lastRevealedLine = node . line ;
1368+ void outlineView . reveal ( node , { select : true , focus : false } ) ;
1369+ } ;
1370+
1371+ context . subscriptions . push (
1372+ outlineView ,
1373+ revealKeywordCommand ,
1374+ vscode . window . onDidChangeActiveTextEditor ( editor => {
1375+ outlineProvider . refresh ( editor ?. document ) ;
1376+ lastRevealedLine = - 1 ;
1377+ } ) ,
1378+ vscode . workspace . onDidChangeTextDocument ( e => {
1379+ if ( e . document === vscode . window . activeTextEditor ?. document ) {
1380+ refreshOutline ( e . document ) ;
1381+ }
1382+ } ) ,
1383+ vscode . window . onDidChangeTextEditorSelection ( e => {
1384+ syncOutlineSelection ( e . textEditor ) ;
1385+ } ) ,
1386+ ) ;
1387+
12611388 // --- Diagnostics: over-arity records and wrong-section keywords ---
12621389 const diagnostics = vscode . languages . createDiagnosticCollection ( 'opm-flow' ) ;
12631390 const refreshDiags = debounce ( ( doc : vscode . TextDocument ) => {
0 commit comments