diff --git a/src/constant/type.ts b/src/constant/type.ts index 8c81b73..20cb3c4 100644 --- a/src/constant/type.ts +++ b/src/constant/type.ts @@ -1,4 +1,6 @@ -export type RouteType = 'static' | 'dynamic' | 'rest' | 'optional' | 'error' | 'layout' | 'divider' | 'group' | 'matcher'; +export type RouteType = 'static' | 'dynamic' | 'rest' | 'optional' | 'error' | 'layout' | 'divider' | 'group' | 'matcher' | 'spacer'; + +export type FileType = 'page' | 'server' | 'layout' | 'error' | 'pageServer' | 'layoutServer' | 'pageClient' | 'layoutClient'; export interface RouteColors { static: string; @@ -23,4 +25,10 @@ export interface RouteMatch { export interface SegmentMatch { remainingSegments: string[]; score: number; +} + +export interface RouteFileInfo { + filePath: string; + fileType: FileType; + resetInfo: ResetInfo | null; } \ No newline at end of file diff --git a/src/models/routeItem.ts b/src/models/routeItem.ts index f0f2054..03a79ae 100644 --- a/src/models/routeItem.ts +++ b/src/models/routeItem.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; -import { ResetInfo, RouteType } from '../constant/type'; +import { FileType, ResetInfo, RouteType } from '../constant/type'; +import { basename } from 'path'; export class RouteItem extends vscode.TreeItem { constructor( @@ -10,7 +11,8 @@ export class RouteItem extends vscode.TreeItem { private port: number, public routeType: RouteType, public isHierarchical: boolean = false, - public resetInfo: ResetInfo | null = null + public resetInfo: ResetInfo | null = null, + public fileType: FileType = 'page', ) { super( label, @@ -20,36 +22,22 @@ export class RouteItem extends vscode.TreeItem { ); // Format the label and description - if (routeType === 'divider') { - this.label = this.formatDividerLabel(label); + if (routeType === 'divider' || routeType === 'spacer') { + this.label = this.formatSpecialLabel(label, routeType); this.description = ''; this.contextValue = 'divider'; this.tooltip = ''; this.command = undefined; this.iconPath = undefined; } else { - // For hierarchical view, keep the bare parameter name - if (isHierarchical) { - // Simpler description for hierarchical view - const parts: string[] = []; - if (this.resetInfo) { - parts.push(`[resets to ${this.resetInfo.displayName.replace(/[()]/g, '')}]`); - } - const matcherMatch = this.routePath.match(/\[(\w+)=(\w+)\]/); - if (matcherMatch) { - parts.push(`[${matcherMatch[2]}]`); - } - this.description = parts.join(' '); - } else { - // Flat view formatting remains the same - this.description = this.formatDescription(); - } - this.label = this.formatDisplayPath(label); + this.description = this.formatDescription(); + this.label = isHierarchical ? label : this.formatDisplayPath(label); // Set icon and color let icon = 'file'; let color = 'charts.green'; + // First determine base icon and color from route type switch (routeType) { case 'error': icon = 'error'; @@ -78,6 +66,27 @@ export class RouteItem extends vscode.TreeItem { default: } + const fileName = basename(filePath); + if (fileName) { + if (fileName.includes('+server.')) { + icon = 'server-process'; + color = 'charts.orange'; + } else if (fileName.includes('.server.')) { + icon = 'server'; + color = 'charts.yellow'; // server-side files get yellow + } else if (fileName.includes('.ts') && !fileName.includes('.server.')) { + icon = 'vm'; + color = 'charts.blue'; // client-side TS files get blue + } else if (fileName.includes('+layout.')) { + icon = 'layout'; + color = 'charts.purple'; + } else if (fileName.includes('+error.')) { + icon = 'error'; + color = 'errorForeground'; + } + // Regular page files will keep their route type colors + } + // ignoring the private constructor error here // @ts-ignore this.iconPath = new vscode.ThemeIcon(icon, new vscode.ThemeColor(color)); @@ -90,22 +99,23 @@ export class RouteItem extends vscode.TreeItem { title: 'Open File', arguments: [this] }; - } - // Enhanced tooltip - this.tooltip = this.getTooltipContent(routePath, routeType, resetInfo, filePath); + // Enhanced tooltip + this.tooltip = this.getTooltipContent(routePath, routeType, resetInfo, filePath, fileType); + } } private isGroupRoute(): boolean { return this.routePath.includes('(') && this.routePath.includes(')'); } - private getTooltipContent(routePath: string, routeType: string, resetInfo: any, filePath: string): string { + private getTooltipContent(routePath: string, routeType: string, resetInfo: any, filePath: string, fileType: string): string { return [ `Path: ${routePath}`, `Type: ${routeType}`, resetInfo ? `Resets to: ${resetInfo.displayName} layout` : '', filePath ? `File: ${filePath}` : '', + fileType ? `Type: ${fileType}` : '', this.isGroupRoute() ? 'Group Route' : '' ].filter(Boolean).join('\n'); } @@ -142,21 +152,41 @@ export class RouteItem extends vscode.TreeItem { layout: 'layout', group: 'group', divider: '', - matcher: 'matcher' + matcher: 'matcher', + spacer: '' }; - // Add page type - if (this.routeType !== 'divider') { - // Check if path contains multiple parameter types + // Determine file type based on filename first + const fileName = this.filePath ? basename(this.filePath) : ''; + + // Add route type only for actual pages (not for layouts, servers, etc) + if (this.routeType !== 'divider' && + (!fileName.includes('+layout.') && + !fileName.includes('+server.'))) { const hasMultipleTypes = (this.routePath?.match(/\[[^\]]+\]/g) ?? []).length > 1; const displayType = hasMultipleTypes ? 'dynamic' : typeMap[this.routeType]; parts.push(`[${displayType}]`); } + // Add file type indicators + if (fileName) { + if (fileName.includes('+server.')) { + parts.push('[api]'); + } else if (fileName.includes('+page.server.') || fileName.includes('+layout.server.')) { + parts.push('[server]'); + } else if (fileName.includes('+error.')) { + parts.push('[error]'); + } else if (fileName.includes('+layout.ts') || fileName.includes('+page.ts')) { + parts.push('[client]'); + } else if (fileName.includes('+layout.')) { + parts.push('[layout]'); + } + } + // Add group info if it's inside a group const groupMatch = this.routePath.match(/\(([^)]+)\)/); if (groupMatch && !this.routePath.startsWith('(')) { - parts.push(`[${groupMatch[1]} group]`); + parts.push(`[${groupMatch[1]}]`); } // Add matcher info if present @@ -173,12 +203,17 @@ export class RouteItem extends vscode.TreeItem { return parts.join(' '); } - private formatDividerLabel(label: string): string { - // Check if it's a root level group + private formatSpecialLabel(label: string, type: RouteType): string { + console.log('formatSpecialLabel', label, type); + if (type === 'spacer') { + return '---------------'; // Simple spacer line + } + + // For dividers (directory or group headers) if (label.startsWith('(') && label.endsWith(')')) { const groupName = label.slice(1, -1); - return `─────── ${groupName} (group) ───────`; + return `───── ${groupName} (group) ─────`; } - return `─────── ${label} ───────`; + return `───── ${label === '/' ? 'root' : label} ─────`; } } \ No newline at end of file diff --git a/src/providers/routesProvider.ts b/src/providers/routesProvider.ts index 68969f1..514b813 100644 --- a/src/providers/routesProvider.ts +++ b/src/providers/routesProvider.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as fs from 'fs'; import { RouteItem } from '../models/routeItem'; import { RouteUtils } from '../utils/routeUtils'; -import { ResetInfo, RouteMatch, RouteType, SegmentMatch } from '../constant/type'; +import { ResetInfo, RouteFileInfo, RouteMatch, RouteType, SegmentMatch } from '../constant/type'; /** * Provider class for managing SvelteKit routes in VS Code @@ -87,6 +87,25 @@ export class RoutesProvider implements vscode.TreeDataProvider { entries.sort((a, b) => this.compareRoutes(a, b)); + // Process root level files + if (!basePath) { + const fileInfos = this.findPageInfo(dir); + for (const fileInfo of fileInfos) { + routes.push(new RouteItem( + '/', + '/', + fileInfo.filePath, + [], + this.port, + 'static', + !this.flatView, // Use hierarchical view flag + fileInfo.resetInfo, + fileInfo.fileType + )); + } + } + + // Process directories for (const entry of entries) { const fullPath = path.join(dir, entry); const stat = fs.statSync(fullPath); @@ -94,37 +113,56 @@ export class RoutesProvider implements vscode.TreeDataProvider { if (stat.isDirectory()) { const routePath = path.join(basePath, entry); const routeType = this.determineRouteType(entry); - const pageInfo = this.findPageInfo(fullPath); + const dirFileInfos = this.findPageInfo(fullPath); const children = this.buildRoutesTree(fullPath, routePath); if (this.flatView) { - // In flat view, only add the route if it has a page file - if (pageInfo.filePath) { + // Flat view logic remains unchanged + for (const fileInfo of dirFileInfos) { routes.push(new RouteItem( routePath, routePath, - pageInfo.filePath, - [], // Empty children array in flat view + fileInfo.filePath, + [], this.port, routeType, false, - pageInfo.resetInfo + fileInfo.resetInfo, + fileInfo.fileType )); } - // Add all children regardless routes.push(...children); } else { - // In hierarchical view - if (pageInfo.filePath || routeType === 'group' || children.length > 0) { - routes.push(new RouteItem( + // Enhanced hierarchical view logic + const routeFiles: RouteItem[] = []; + + // Add all directory files as direct children + for (const fileInfo of dirFileInfos) { + routeFiles.push(new RouteItem( + path.basename(fileInfo.filePath), routePath, + fileInfo.filePath, + [], + this.port, + routeType, + true, + fileInfo.resetInfo, + fileInfo.fileType + )); + } + + // Create directory node with all children + if (routeFiles.length > 0 || children.length > 0) { + routes.push(new RouteItem( + entry, routePath, - pageInfo.filePath, - children, + dirFileInfos[0]?.filePath || '', + [...routeFiles, ...children], this.port, routeType, true, - pageInfo.resetInfo + dirFileInfos[0]?.resetInfo || null, + dirFileInfos[0]?.fileType || 'page' )); } } @@ -150,35 +188,88 @@ export class RoutesProvider implements vscode.TreeDataProvider { return 'static'; } - private findPageInfo(dir: string): { filePath: string; resetInfo: ResetInfo | null } { + private findPageInfo(dir: string): RouteFileInfo[] { const files = fs.readdirSync(dir); + const fileInfos: RouteFileInfo[] = []; - // Check for reset pages first + // Check each file in the directory for (const file of files) { + // Skip non-route files + if (!file.startsWith('+')) { + continue; + } + + // Check for reset pages first if (file.includes('+page@')) { const resetInfo = this.parseResetInfo(file); - if (resetInfo) { - return { - filePath: path.join(dir, file), - resetInfo - }; - } + fileInfos.push({ + filePath: path.join(dir, file), + fileType: 'page', + resetInfo + }); + continue; } - } - // Check for regular page - const regularPage = files.find(f => f === '+page.svelte'); - if (regularPage) { - return { - filePath: path.join(dir, regularPage), - resetInfo: null - }; + // Determine file type + if (file.startsWith('+page.svelte')) { + fileInfos.push({ + filePath: path.join(dir, file), + fileType: 'page', + resetInfo: null + }); + } + else if (file.startsWith('+page.ts')) { + fileInfos.push({ + filePath: path.join(dir, file), + fileType: 'pageClient', + resetInfo: null + }); + } + else if (file.startsWith('+page.server.ts')) { + fileInfos.push({ + filePath: path.join(dir, file), + fileType: 'pageServer', + resetInfo: null + }); + } + else if (file.startsWith('+server.ts')) { + fileInfos.push({ + filePath: path.join(dir, file), + fileType: 'server', + resetInfo: null + }); + } + else if (file.startsWith('+layout.svelte')) { + fileInfos.push({ + filePath: path.join(dir, file), + fileType: 'layout', + resetInfo: null + }); + } + else if (file.startsWith('+layout.ts')) { + fileInfos.push({ + filePath: path.join(dir, file), + fileType: 'layoutClient', + resetInfo: null + }); + } + else if (file.startsWith('+layout.server.ts')) { + fileInfos.push({ + filePath: path.join(dir, file), + fileType: 'layoutServer', + resetInfo: null + }); + } + else if (file.startsWith('+error.svelte')) { + fileInfos.push({ + filePath: path.join(dir, file), + fileType: 'error', + resetInfo: null + }); + } } - return { - filePath: '', - resetInfo: null - }; + return fileInfos; } private parseResetInfo(fileName: string): ResetInfo | null { @@ -195,20 +286,38 @@ export class RoutesProvider implements vscode.TreeDataProvider { private flattenRoutes(routes: RouteItem[]): RouteItem[] { const routeGroups = new Map(); + let lastSubDirectory = ''; const processRoute = (item: RouteItem) => { const segments = item.routePath.split('\\'); const topLevel = segments[0] || 'root'; + // Get the subdirectory if it exists (e.g., 'test/about', 'test/blog') + const subDir = segments.length > 1 ? segments.slice(0, 2).join('/') : ''; + if (!routeGroups.has(topLevel)) { routeGroups.set(topLevel, []); } - // Add the route with its full path - let displayName = item.routePath; + // Add spacer if we're switching to a new subdirectory + if (subDir && subDir !== lastSubDirectory && lastSubDirectory !== '') { + routeGroups.get(topLevel)?.push(new RouteItem( + '', + '', + '', + [], + this.port, + 'spacer' + )); + } + if (subDir) { + lastSubDirectory = subDir; + } + + // Add the route with its full path routeGroups.get(topLevel)?.push(new RouteItem( - displayName, + item.routePath, item.routePath, item.filePath, [],