Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Or search for "Outline Eclipsed" in the Extensions view (`Ctrl+Shift+X`).

### Language Support

- ✅ **Markdown** — H1–H6 headings with custom provider (full drag & drop)
- ✅ **Markdown** — H1–H6 headings, code blocks, quote blocks, and images with custom provider (full drag & drop for headings)
- ✅ **TypeScript/JavaScript** — Classes, functions, methods, properties (view-only outline; drag & drop disabled)
- ✅ **Python** — Classes, functions, methods (view-only outline; drag & drop disabled)
- ✅ **Java** — Classes, methods, fields (view-only outline; drag & drop disabled)
Expand Down Expand Up @@ -85,7 +85,7 @@ Press **F5** to launch the Extension Development Host, then open `test-fixtures/
- ✅ **PI-8**: Multi-language outline viewing (JavaScript, TypeScript, Python) — drag & drop deferred
- ✅ **PI-9**: Rich tree item descriptions and tooltips — line ranges and symbol information
- 🔲 **PI-10**: Show outline for markdown preview when focused
- 🔲 **PI-11**: Additional markdown symbols (code blocks, quotes)
- **PI-11**: Additional markdown symbols (code blocks, quote blocks, images)
- 🔲 **PI-12**: Configuration options
- 🔲 **Future**: Enable drag & drop for additional languages; advanced customization

Expand Down
20 changes: 19 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
- Rich tooltips with symbol name, type, and line information
- Works across all supported languages
- 🔲 **PI-10**: show outline for markdown preview when focused
- 🔲 **PI-11**: add addition symbols for markdown e.g. code blocks, quotes
- ✅ **PI-11**: Additional markdown symbols (code blocks, quote blocks, images)
- Custom parser extends VS Code's built-in markdown support
- TDD approach with unit tests independent of VS Code API
- Fenced code blocks (``` and ~~~) with language labels
- Quote blocks (> lines, including multi-line)
- Images (![alt](url)) with alt text or filename labels
- Symbols nested under heading hierarchy
- 🔲 **PI-12**: Configuration options
- 🔲 **Future**: Enable drag & drop for additional languages; advanced customization

Expand Down Expand Up @@ -76,3 +82,15 @@
- Consistent formatting across all supported languages
- Markdown tooltips for better readability
- Tested with Markdown, TypeScript, JavaScript, and Python

### PI-11: Additional Markdown Symbols ✅
- VS Code's built-in markdown extension only supports headings for outline
- Custom standalone parser (`markdownParser.ts`) extends support to:
- **Fenced Code Blocks**: ``` and ~~~ syntax with language detection
- **Quote Blocks**: Lines starting with > (supports multi-line)
- **Images**: ![alt](url) syntax with alt text or filename labels
- TDD approach with unit tests independent of VS Code API
- Integration tests for VS Code API collaboration
- Symbols nested under their parent headings in hierarchy
- Level 7 for non-heading symbols (below H6 in hierarchy)
- Tested with test-fixtures/PI-11-test.md
169 changes: 169 additions & 0 deletions src/markdownOutlineProvider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import * as vscode from 'vscode';
import { GenericOutlineProvider } from './genericOutlineProvider';
import { OutlineItem } from './outlineItem';
import {
parseMarkdownSymbols,
splitIntoLines,
MarkdownSymbol,
MarkdownSymbolType
} from './markdownParser';

/**
* Markdown-specific outline provider.
* Extends GenericOutlineProvider with markdown-specific symbol name sanitization
* and level mapping for heading hierarchies.
*
* PI-2: Builds hierarchical tree based on heading levels (H1-H6)
* PI-11: Adds support for code blocks, quote blocks, and images
*/
export class MarkdownOutlineProvider extends GenericOutlineProvider {

Expand Down Expand Up @@ -54,4 +62,165 @@ export class MarkdownOutlineProvider extends GenericOutlineProvider {
return super.getLevelFromSymbol(symbol);
}
}

/**
* PI-11: Override parseDocument to combine VS Code headings with custom parser symbols.
*
* Strategy:
* 1. Get heading symbols from VS Code's built-in markdown parser (via super.parseDocument)
* 2. Parse additional symbols (code blocks, quotes, images) with custom parser
* 3. Merge and sort all symbols by line position
* 4. Build hierarchy with headings as parents and other symbols as leaves
*
* @param document - Document to parse
* @returns Array of root-level outline items with nested children
*/
protected async parseDocument(document: vscode.TextDocument): Promise<OutlineItem[]> {
// Step 1: Get headings from VS Code's built-in parser
const headingItems = await super.parseDocument(document);

// Step 2: Parse additional symbols with custom parser
const text = document.getText();
const lines = splitIntoLines(text);
const customSymbols = parseMarkdownSymbols(lines);

// Filter out headings from custom parser (we already have them from VS Code)
// and convert to OutlineItems
const additionalSymbols = customSymbols
.filter(s => s.type !== MarkdownSymbolType.Heading)
.map(s => this.convertToOutlineItem(s, document));

// Step 3: Merge all items and sort by start line
const allItems = [...this.flattenItems(headingItems), ...additionalSymbols];
allItems.sort((a, b) => a.range.start.line - b.range.start.line);

// Step 4: Build hierarchy with headings containing other symbols
return this.buildEnhancedHierarchy(allItems);
}

/**
* Converts a MarkdownSymbol to an OutlineItem.
*
* @param symbol - The parsed markdown symbol
* @param document - The source document
* @returns OutlineItem for the symbol
*/
private convertToOutlineItem(symbol: MarkdownSymbol, document: vscode.TextDocument): OutlineItem {
const range = new vscode.Range(
symbol.startLine, symbol.startChar,
symbol.endLine, symbol.endChar
);

const selectionRange = new vscode.Range(
symbol.startLine, symbol.startChar,
symbol.startLine, document.lineAt(symbol.startLine).text.length
);

// Map symbol type to VS Code SymbolKind
const symbolKind = this.getSymbolKindForType(symbol.type);

return new OutlineItem(
symbol.label,
symbol.level,
range,
selectionRange,
[], // Children will be populated by buildEnhancedHierarchy
symbolKind,
document,
symbol.detail
);
}

/**
* Maps MarkdownSymbolType to VS Code SymbolKind for proper icons.
*/
private getSymbolKindForType(type: MarkdownSymbolType): vscode.SymbolKind {
switch (type) {
case MarkdownSymbolType.CodeBlock:
return vscode.SymbolKind.Struct; // Code block icon
case MarkdownSymbolType.QuoteBlock:
return vscode.SymbolKind.String; // Quote uses string icon
case MarkdownSymbolType.Image:
return vscode.SymbolKind.File; // Image uses file icon
default:
return vscode.SymbolKind.String;
}
}

/**
* Flattens a hierarchical tree of items into a flat list.
* Preserves all items including nested children.
*/
private flattenItems(items: OutlineItem[]): OutlineItem[] {
const result: OutlineItem[] = [];

const flatten = (item: OutlineItem) => {
// Clone the item without children for flattening
const flatItem = new OutlineItem(
item.label,
item.level,
item.range,
item.selectionRange,
[], // Clear children - will be rebuilt
item.symbolKind,
undefined, // No document needed for cloning
undefined
);
result.push(flatItem);

// Recursively flatten children
for (const child of item.children) {
flatten(child);
}
};

for (const item of items) {
flatten(item);
}

return result;
}

/**
* PI-11: Builds enhanced hierarchy where headings are parents.
* Non-heading symbols (code blocks, quotes, images) become children of
* the heading section they appear in.
*
* @param flatItems - Flat array of all items sorted by line position
* @returns Array of root-level items with nested children
*/
private buildEnhancedHierarchy(flatItems: OutlineItem[]): OutlineItem[] {
if (flatItems.length === 0) {
return [];
}

const rootItems: OutlineItem[] = [];
const stack: OutlineItem[] = [];

for (const item of flatItems) {
// Pop items from stack until we find a valid parent (lower level number = higher in hierarchy)
while (stack.length > 0 && stack[stack.length - 1].level >= item.level) {
stack.pop();
}

if (stack.length === 0) {
// No parent - this is a root item
item.parent = undefined;
rootItems.push(item);
} else {
// Add as child of the item at top of stack
const parent = stack[stack.length - 1];
item.parent = parent;
parent.children.push(item);
}

// Only push headings onto stack (they can have children)
// Non-heading symbols (level 7) should not become parents
if (item.level < 7) {
stack.push(item);
}
}

return rootItems;
}
}
Loading