Skip to content

Commit de890b5

Browse files
committed
initial pass at adding editory -> outline positional highlgihting #29
1 parent 6f94d1c commit de890b5

4 files changed

Lines changed: 311 additions & 4 deletions

File tree

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,25 @@
117117
}
118118
}
119119
},
120+
"views": {
121+
"explorer": [
122+
{
123+
"id": "codeOrganizerOutline",
124+
"name": "Code Organizer",
125+
"when": "resourceLangId"
126+
}
127+
]
128+
},
120129
"commands": [
121130
{
122131
"command": "codeOrganizer.activate",
123132
"title": "Activate",
124133
"category": "Code Organizer"
134+
},
135+
{
136+
"command": "codeOrganizer.goToSection",
137+
"title": "Go to Section",
138+
"category": "Code Organizer"
125139
}
126140
]
127141
},

src/decorations.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as vscode from 'vscode';
2+
import { SectionMatch } from './utils/findSections';
3+
4+
let currentSectionDecoration: vscode.TextEditorDecorationType | undefined;
5+
6+
export function initializeDecorations(): vscode.TextEditorDecorationType {
7+
if (!currentSectionDecoration) {
8+
currentSectionDecoration = vscode.window.createTextEditorDecorationType({
9+
isWholeLine: true,
10+
backgroundColor: new vscode.ThemeColor('editor.lineHighlightBackground'),
11+
borderWidth: '0 0 0 3px',
12+
borderStyle: 'solid',
13+
borderColor: new vscode.ThemeColor('editorInfo.foreground'),
14+
overviewRulerColor: new vscode.ThemeColor('editorInfo.foreground'),
15+
overviewRulerLane: vscode.OverviewRulerLane.Left
16+
});
17+
}
18+
return currentSectionDecoration;
19+
}
20+
21+
export function updateSectionHighlight(
22+
section: SectionMatch | undefined,
23+
editor: vscode.TextEditor,
24+
decoration: vscode.TextEditorDecorationType
25+
): void {
26+
if (!section) {
27+
editor.setDecorations(decoration, []);
28+
return;
29+
}
30+
31+
const startPos = editor.document.positionAt(section.index);
32+
const endPos = editor.document.positionAt(section.index + section.fullText.length);
33+
const range = new vscode.Range(startPos, endPos);
34+
35+
const options: vscode.DecorationOptions[] = [{
36+
range: range,
37+
hoverMessage: `📍 Current Section: **${section.name}**`
38+
}];
39+
40+
editor.setDecorations(decoration, options);
41+
}
42+
43+
export function disposeDecorations(): void {
44+
currentSectionDecoration?.dispose();
45+
currentSectionDecoration = undefined;
46+
}

src/extension.ts

Lines changed: 147 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import * as vscode from 'vscode';
22
import { CodeOrganizerDocumentSymbolProvider } from './documentSymbolProvider';
3+
import { CodeOrganizerTreeDataProvider } from './treeDataProvider';
4+
import { initializeDecorations, updateSectionHighlight, disposeDecorations } from './decorations';
5+
import { SectionMatch } from './utils/findSections';
36

47
export function activate(context: vscode.ExtensionContext) {
5-
8+
69
// Show activation message
710
vscode.window.showInformationMessage('Code Organizer extension activated!');
8-
11+
912
// Get configuration
1013
const config = vscode.workspace.getConfiguration('codeOrganizer');
1114
const isEnabled = config.get<boolean>('enable', true);
@@ -16,7 +19,7 @@ export function activate(context: vscode.ExtensionContext) {
1619
return;
1720
}
1821

19-
// Register document symbol provider
22+
// Register document symbol provider (keep for built-in Outline)
2023
const provider = new CodeOrganizerDocumentSymbolProvider();
2124

2225
if (supportedLanguages.includes('*')) {
@@ -39,6 +42,145 @@ export function activate(context: vscode.ExtensionContext) {
3942
});
4043
}
4144

45+
// Create custom TreeView for enhanced outline with highlighting
46+
const treeDataProvider = new CodeOrganizerTreeDataProvider();
47+
const treeView = vscode.window.createTreeView('codeOrganizerOutline', {
48+
treeDataProvider: treeDataProvider,
49+
showCollapseAll: true
50+
});
51+
context.subscriptions.push(treeView);
52+
console.log('[Code Organizer] TreeView created');
53+
54+
// Initialize editor decorations
55+
const decoration = initializeDecorations();
56+
context.subscriptions.push(decoration);
57+
58+
// Register "Go to Section" command
59+
context.subscriptions.push(
60+
vscode.commands.registerCommand(
61+
'codeOrganizer.goToSection',
62+
(section: SectionMatch, document: vscode.TextDocument) => {
63+
const editor = vscode.window.activeTextEditor;
64+
if (editor && editor.document === document) {
65+
const position = document.positionAt(section.index);
66+
editor.selection = new vscode.Selection(position, position);
67+
editor.revealRange(
68+
new vscode.Range(position, position),
69+
vscode.TextEditorRevealType.InCenter
70+
);
71+
}
72+
}
73+
)
74+
);
75+
76+
// Helper function to find current section from cursor position
77+
function getCurrentSection(
78+
cursorPos: vscode.Position,
79+
document: vscode.TextDocument,
80+
sections: SectionMatch[]
81+
): SectionMatch | undefined {
82+
const offset = document.offsetAt(cursorPos);
83+
let deepestSection: SectionMatch | undefined;
84+
85+
for (const section of sections) {
86+
const sectionStart = section.index;
87+
// Find next section at same or higher level to determine end
88+
const nextSection = sections.find(
89+
s => s.index > section.index && s.depth <= section.depth
90+
);
91+
const sectionEnd = nextSection ? nextSection.index : document.getText().length;
92+
93+
if (offset >= sectionStart && offset < sectionEnd) {
94+
// Found a containing section, keep the deepest one
95+
if (!deepestSection || section.depth > deepestSection.depth) {
96+
deepestSection = section;
97+
}
98+
}
99+
}
100+
101+
return deepestSection;
102+
}
103+
104+
// Update highlight function with caching
105+
let updateTimeout: NodeJS.Timeout | undefined;
106+
let lastDocument: vscode.TextDocument | undefined;
107+
108+
async function updateHighlight() {
109+
const editor = vscode.window.activeTextEditor;
110+
if (!editor) {
111+
console.log('[Code Organizer] No active editor');
112+
return;
113+
}
114+
115+
const document = editor.document;
116+
117+
// Refresh tree if document changed
118+
if (document !== lastDocument) {
119+
console.log('[Code Organizer] Refreshing tree for document:', document.fileName);
120+
treeDataProvider.refresh(document);
121+
lastDocument = document;
122+
}
123+
124+
const cursorPos = editor.selection.active;
125+
const sections = treeDataProvider.getSections();
126+
console.log('[Code Organizer] Found', sections.length, 'sections');
127+
const currentSection = getCurrentSection(cursorPos, document, sections);
128+
console.log('[Code Organizer] Current section:', currentSection?.name);
129+
130+
// Update editor decoration
131+
updateSectionHighlight(currentSection, editor, decoration);
132+
133+
// Update tree view highlight
134+
if (currentSection) {
135+
// Get the cached tree item instance for reveal to work
136+
const item = treeDataProvider.findTreeItemBySection(currentSection);
137+
console.log('[Code Organizer] Tree item found:', !!item);
138+
if (item) {
139+
try {
140+
await treeView.reveal(item, {
141+
select: true,
142+
focus: false,
143+
expand: 1
144+
});
145+
console.log('[Code Organizer] Reveal succeeded');
146+
} catch (error) {
147+
console.error('[Code Organizer] Reveal failed:', error);
148+
}
149+
}
150+
}
151+
}
152+
153+
// Listen for cursor changes (with debouncing)
154+
context.subscriptions.push(
155+
vscode.window.onDidChangeTextEditorSelection(() => {
156+
if (updateTimeout) {
157+
clearTimeout(updateTimeout);
158+
}
159+
updateTimeout = setTimeout(updateHighlight, 150);
160+
})
161+
);
162+
163+
// Listen for editor switches
164+
context.subscriptions.push(
165+
vscode.window.onDidChangeActiveTextEditor(() => {
166+
updateHighlight();
167+
})
168+
);
169+
170+
// Clear cache on document changes
171+
context.subscriptions.push(
172+
vscode.workspace.onDidChangeTextDocument((event) => {
173+
if (event.document === lastDocument) {
174+
lastDocument = undefined;
175+
}
176+
})
177+
);
178+
179+
// Initial highlight
180+
if (vscode.window.activeTextEditor) {
181+
updateHighlight();
182+
}
183+
42184
// Register activation command as fallback
43185
const activateCommand = vscode.commands.registerCommand('codeOrganizer.activate', () => {
44186
vscode.window.showInformationMessage('Code Organizer is already active and working!');
@@ -63,5 +205,6 @@ export function activate(context: vscode.ExtensionContext) {
63205
}
64206

65207
export function deactivate() {
66-
// Cleanup if needed
208+
// Cleanup decorations
209+
disposeDecorations();
67210
}

src/treeDataProvider.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import * as vscode from 'vscode';
2+
import { SectionMatch, findSections } from './utils/findSections';
3+
4+
export class SectionTreeItem extends vscode.TreeItem {
5+
constructor(
6+
public readonly section: SectionMatch,
7+
private allSections: SectionMatch[],
8+
public readonly document: vscode.TextDocument
9+
) {
10+
const hasChildren = allSections.some(s => s.parentName === section.uniqueId);
11+
super(
12+
section.name,
13+
hasChildren
14+
? vscode.TreeItemCollapsibleState.Expanded
15+
: vscode.TreeItemCollapsibleState.None
16+
);
17+
18+
this.tooltip = section.name;
19+
20+
// Set icon based on depth
21+
this.iconPath = new vscode.ThemeIcon(
22+
section.depth === 1 ? 'symbol-module' :
23+
section.depth === 2 ? 'symbol-class' :
24+
section.depth === 3 ? 'symbol-method' : 'symbol-property'
25+
);
26+
27+
// Command to jump to section
28+
this.command = {
29+
command: 'codeOrganizer.goToSection',
30+
title: 'Go to Section',
31+
arguments: [section, document]
32+
};
33+
34+
this.contextValue = 'sectionItem';
35+
}
36+
}
37+
38+
export class CodeOrganizerTreeDataProvider implements vscode.TreeDataProvider<SectionTreeItem> {
39+
private _onDidChangeTreeData = new vscode.EventEmitter<SectionTreeItem | undefined | null>();
40+
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
41+
42+
private sections: SectionMatch[] = [];
43+
private currentDocument?: vscode.TextDocument;
44+
private treeItemCache: Map<string, SectionTreeItem> = new Map();
45+
46+
refresh(document: vscode.TextDocument): void {
47+
this.currentDocument = document;
48+
this.sections = findSections(document.getText(), document.languageId);
49+
this.treeItemCache.clear();
50+
this._onDidChangeTreeData.fire(undefined);
51+
}
52+
53+
getTreeItem(element: SectionTreeItem): vscode.TreeItem {
54+
return element;
55+
}
56+
57+
getChildren(element?: SectionTreeItem): SectionTreeItem[] {
58+
if (!this.currentDocument) {
59+
return [];
60+
}
61+
62+
if (!element) {
63+
// Root level - return depth 1 sections
64+
return this.sections
65+
.filter(s => s.depth === 1)
66+
.map(s => this.getOrCreateTreeItem(s));
67+
} else {
68+
// Return children of this section
69+
return this.sections
70+
.filter(s => s.parentName === element.section.uniqueId)
71+
.map(s => this.getOrCreateTreeItem(s));
72+
}
73+
}
74+
75+
getParent(element: SectionTreeItem): SectionTreeItem | undefined {
76+
const parentSection = this.sections.find(s => s.uniqueId === element.section.parentName);
77+
if (parentSection) {
78+
return this.getOrCreateTreeItem(parentSection);
79+
}
80+
return undefined;
81+
}
82+
83+
private getOrCreateTreeItem(section: SectionMatch): SectionTreeItem {
84+
const cached = this.treeItemCache.get(section.uniqueId);
85+
if (cached) {
86+
return cached;
87+
}
88+
const item = new SectionTreeItem(section, this.sections, this.currentDocument!);
89+
this.treeItemCache.set(section.uniqueId, item);
90+
return item;
91+
}
92+
93+
findTreeItemBySection(section: SectionMatch): SectionTreeItem | undefined {
94+
return this.treeItemCache.get(section.uniqueId);
95+
}
96+
97+
getSections(): SectionMatch[] {
98+
return this.sections;
99+
}
100+
101+
getCurrentDocument(): vscode.TextDocument | undefined {
102+
return this.currentDocument;
103+
}
104+
}

0 commit comments

Comments
 (0)