Skip to content
Open
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
17 changes: 14 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@ import {
graphDataAtom,
parseErrorAtom,
parseProgressAtom,
rightPanelOpenAtom,
rightPanelTypeAtom,
viewModeAtom,
} from '@/entities/AppView/model/atoms';
import { store } from '@/entities/AppView/model/store';
import { UnifiedSearchModal } from '@/features/Search/UnifiedSearch/ui/UnifiedSearchModal';
import { JsonExplorer } from '@/pages/JsonExplorer/JsonExplorer';
import { deadCodePanelOpenAtom } from '@/pages/PageAnalysis/DeadCodePanel/model/atoms';
import { PageAnalysis } from '@/pages/PageAnalysis/PageAnalysis';
import IDEScrollView from '@/widgets/MainContents/IDEScrollView/IDEScrollView';
import PipelineCanvas from '@/widgets/MainContents/PipelineCanvas/PipelineCanvas.tsx';
import { TabContainer } from '@/widgets/MainContents/TabContainer';
import type { SourceFileNode } from './entities/SourceFileNode/model/types';
import { KeyboardShortcuts } from './features/KeyboardShortcuts/KeyboardShortcuts';
import CodeDocView from './widgets/CodeDocView/CodeDocView';
import { WorkspacePanel } from './widgets/WorkspacePanel/WorkspacePanel';

const AppContent: React.FC = () => {
// Parse project when files change
Expand All @@ -34,6 +37,9 @@ const AppContent: React.FC = () => {
const setParseProgress = useSetAtom(parseProgressAtom);
const viewMode = useAtomValue(viewModeAtom);
const deadCodePanelOpen = useAtomValue(deadCodePanelOpenAtom);
const rightPanelOpen = useAtomValue(rightPanelOpenAtom);
const rightPanelType = useAtomValue(rightPanelTypeAtom);
const setRightPanelOpen = useSetAtom(rightPanelOpenAtom);
const workerRef = useRef<Worker | null>(null);

// 🔥 Web Worker for Project Parsing
Expand Down Expand Up @@ -162,12 +168,17 @@ const AppContent: React.FC = () => {
{/* Left Sidebar: File Explorer */}
<AppSidebar />

{/* Main Content Area: Canvas or IDEScrollView or CodeDocView */}
{/* Main Content Area: Canvas or TabContainer (IDE/Search) or CodeDocView */}
<div className="flex-1 relative overflow-hidden">
{viewMode === 'canvas' && <PipelineCanvas />}
{viewMode === 'ide' && <IDEScrollView />}
{(viewMode === 'ide' || viewMode === 'contentSearch') && <TabContainer />}
{viewMode === 'codeDoc' && <CodeDocView />}
</div>

{/* Right Panel: Workspace */}
{rightPanelOpen && rightPanelType === 'workspace' && (
<WorkspacePanel onClose={() => setRightPanelOpen(false)} />
)}
</>
)}
</div>
Expand Down
89 changes: 38 additions & 51 deletions src/app/ui/AppSidebar/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,80 +3,34 @@
* Provides resizable sidebar layout for file navigation
*/

import { useAtomValue } from 'jotai';
import { useAtom, useAtomValue } from 'jotai';
import { ChevronDown, ChevronRight } from 'lucide-react';
import type React from 'react';
import { useRef, useState } from 'react';
import { Sidebar } from '@/components/ide/Sidebar.tsx';
import { viewModeAtom } from '@/entities/AppView/model/atoms';
import { useOpenFile } from '@/features/File/OpenFiles/lib/useOpenFile.ts';
import { activeTabAtom, openedTabsAtom } from '@/features/File/OpenFiles/model/atoms.ts';
import { FileIcon } from '../../../entities/SourceFileNode/ui/FileIcon.tsx';
import { getFileName } from '../../../shared/pathUtils.ts';
import { FileExplorer } from '../../../widgets/FileExplorer/FileExplorer.tsx';
import { isSidebarOpenAtom } from './model/atoms.ts';
import { RelatedFilesView } from '../../../widgets/RelatedFilesView/RelatedFilesView.tsx';
import { fileTreeModeAtom, isSidebarOpenAtom } from './model/atoms.ts';

export const AppSidebar: React.FC = () => {
const isSidebarOpen = useAtomValue(isSidebarOpenAtom);
const _viewMode = useAtomValue(viewModeAtom);
const openedTabs = useAtomValue(openedTabsAtom);
const activeTab = useAtomValue(activeTabAtom);
const { openFile } = useOpenFile();
const [fileTreeMode, setFileTreeMode] = useAtom(fileTreeModeAtom);
const containerRef = useRef<HTMLDivElement>(null);

// Collapsible states
const [isOpenedFilesCollapsed, setIsOpenedFilesCollapsed] = useState(false);
const [isFileExplorerCollapsed, setIsFileExplorerCollapsed] = useState(false);

if (!isSidebarOpen) {
return null;
}

const workspaceLabel = 'Workspace';
const projectLabel = 'Project';

return (
<div ref={containerRef} className="relative focus:outline-none">
<Sidebar resizable defaultWidth={250} minWidth={200} maxWidth={800} className="h-full shadow-2xl">
{/* WORKSPACE */}
{openedTabs.length > 0 && (
<div className={isFileExplorerCollapsed ? 'flex-1 flex flex-col overflow-hidden' : ''}>
<button
onClick={() => setIsOpenedFilesCollapsed(!isOpenedFilesCollapsed)}
className="flex w-full h-8 items-center justify-between border-b border-border-DEFAULT px-2 flex-shrink-0 hover:bg-bg-deep transition-colors"
>
<span className="text-2xs font-medium text-text-tertiary normal-case">{workspaceLabel}</span>
{isOpenedFilesCollapsed ? (
<ChevronRight className="w-3 h-3 text-text-muted" />
) : (
<ChevronDown className="w-3 h-3 text-text-muted" />
)}
</button>
{!isOpenedFilesCollapsed && (
<div className="flex-1 flex flex-col overflow-y-auto border-b border-border-DEFAULT">
{openedTabs.map((filePath) => {
const fileName = getFileName(filePath);
const isActive = filePath === activeTab;

return (
<button
key={filePath}
onClick={() => openFile(filePath)}
className={`flex items-center gap-2 px-3 py-1.5 text-xs text-left hover:bg-bg-deep transition-colors ${
isActive ? 'bg-bg-deep text-text-primary' : 'text-text-secondary'
}`}
title={filePath}
>
<FileIcon fileName={fileName} size={16} />
<span className="truncate">{fileName}</span>
</button>
);
})}
</div>
)}
</div>
)}

{/* PROJECT */}
<div className={!isFileExplorerCollapsed ? 'flex-1 flex flex-col overflow-hidden' : ''}>
<button
Expand All @@ -90,7 +44,40 @@ export const AppSidebar: React.FC = () => {
<ChevronDown className="w-3 h-3 text-text-muted" />
)}
</button>
{!isFileExplorerCollapsed && <FileExplorer containerRef={containerRef} />}

{!isFileExplorerCollapsed && (
<>
{/* Mode Tabs */}
<div className="flex border-b border-border-DEFAULT">
<button
onClick={() => setFileTreeMode('all')}
className={`flex-1 px-2 py-1.5 text-2xs font-medium transition-colors ${
fileTreeMode === 'all'
? 'bg-bg-deep text-text-primary border-b-2 border-warm-300'
: 'text-text-tertiary hover:text-text-secondary'
}`}
>
All Files
</button>
<button
onClick={() => setFileTreeMode('related')}
className={`flex-1 px-2 py-1.5 text-2xs font-medium transition-colors ${
fileTreeMode === 'related'
? 'bg-bg-deep text-text-primary border-b-2 border-warm-300'
: 'text-text-tertiary hover:text-text-secondary'
}`}
>
Related
</button>
</div>

{fileTreeMode === 'all' ? (
<FileExplorer containerRef={containerRef} />
) : (
<RelatedFilesView containerRef={containerRef} />
)}
</>
)}
</div>
</Sidebar>
</div>
Expand Down
7 changes: 6 additions & 1 deletion src/app/ui/AppSidebar/model/atoms.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
/**
* AppSidebar Widget - Atoms
* 사이드바 표시 여부 상태
* 사이드바 표시 여부 및 파일 트리 모드 상태
*/
import { atom } from 'jotai';

// 사이드바 열림/닫힘 상태 (Cmd/Ctrl + \ 토글)
export const isSidebarOpenAtom = atom<boolean>(true);

// 파일 트리 모드: 'all' | 'related'
// all: 모든 파일 표시
// related: 활성 파일과 관련된 파일만 표시 (dependencies + dependents)
export const fileTreeModeAtom = atom<'all' | 'related'>('all');
32 changes: 7 additions & 25 deletions src/app/ui/AppTitleBar/AppTitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { useAtom } from 'jotai';
import { ListTree, Network, X } from 'lucide-react';
import { FolderOpen, X } from 'lucide-react';
import { TitleBar } from '@/components/ide/TitleBar.tsx';
import { rightPanelOpenAtom, rightPanelTypeAtom } from '@/entities/AppView/model/atoms';

Expand All @@ -19,40 +19,22 @@ export function AppTitleBar() {
<TitleBar filename={activeFileName} projectName="teo.v">
{/* Right Panel Tabs */}
<div className="flex items-center gap-0.5 border border-border-DEFAULT rounded overflow-hidden">
{/* Definition Tab */}
{/* Workspace Tab */}
<button
type="button"
className={`flex items-center gap-1.5 px-2 py-1 text-xs transition-colors ${
rightPanelOpen && rightPanelType === 'definition'
rightPanelOpen && rightPanelType === 'workspace'
? 'bg-warm-300/15 text-warm-300'
: 'text-text-secondary hover:text-text-primary hover:bg-bg-deep/50'
}`}
onClick={() => {
setRightPanelType('definition');
setRightPanelType('workspace');
if (!rightPanelOpen) setRightPanelOpen(true);
}}
title="Definitions Panel"
title="Workspace Panel"
>
<ListTree size={14} />
<span>Definitions</span>
</button>

{/* Related Tab */}
<button
type="button"
className={`flex items-center gap-1.5 px-2 py-1 text-xs border-l border-border-DEFAULT transition-colors ${
rightPanelOpen && rightPanelType === 'related'
? 'bg-warm-300/15 text-warm-300'
: 'text-text-secondary hover:text-text-primary hover:bg-bg-deep/50'
}`}
onClick={() => {
setRightPanelType('related');
if (!rightPanelOpen) setRightPanelOpen(true);
}}
title="Related Files Panel"
>
<Network size={14} />
<span>Related</span>
<FolderOpen size={14} />
<span>Workspace</span>
</button>

{/* Close Button */}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ide/OutlinePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChevronRight, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import type { OutlineNode } from '../../shared/outlineExtractor';
import type { DefinitionSymbol } from '../../entities/SourceFileNode/lib/definitionExtractor';
import type { OutlineNode } from '../../shared/outlineExtractor';
import { OutlinePanelItem } from './OutlinePanelItem';

export interface OutlinePanelProps {
Expand Down
11 changes: 7 additions & 4 deletions src/entities/AppView/model/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export const fullNodeMapAtom = atom((get) => {
// View Mode Atoms
// ============================================================================

// 뷰 모드 - Canvas vs IDE vs CodeDoc vs JsonExplorer view (localStorage 저장)
export type ViewMode = 'canvas' | 'ide' | 'codeDoc' | 'jsonExplorer';
// 뷰 모드 - Canvas vs IDE vs CodeDoc vs JsonExplorer vs ContentSearch view (localStorage 저장)
export type ViewMode = 'canvas' | 'ide' | 'codeDoc' | 'jsonExplorer' | 'contentSearch';
export const viewModeAtom = atomWithStorage<ViewMode>('viewMode', 'ide'); // Default to IDE mode

// 문서 모드 - Dark vs Light (for CodeDocView)
Expand Down Expand Up @@ -83,5 +83,8 @@ export const hoveredIdentifierAtom = atom<string | null>(null);
// 우측 패널 표시 여부 (기본값: true - 미리 열어둠)
export const rightPanelOpenAtom = atom<boolean>(true);

// 우측 패널 타입 ('definition' | 'related')
export const rightPanelTypeAtom = atom<'definition' | 'related'>('definition');
// 우측 패널 타입 ('workspace' | 'definition' | 'related')
// workspace: 열린 파일 목록
// definition: 파일 정의/아웃라인 (삭제됨, 레거시)
// related: 관련 파일 (삭제됨, 레거시)
export const rightPanelTypeAtom = atom<'workspace' | 'definition' | 'related'>('workspace');
2 changes: 1 addition & 1 deletion src/entities/SourceFileNode/lib/definitionExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
*/

import ts from 'typescript';
import type { SourceFileNode } from '../model/types';
import { createLanguageService } from '../../../shared/tsParser/utils/languageService';
import type { SourceFileNode } from '../model/types';

// Re-export OutlinePanel types (from LIMN component)
export type SymbolKind =
Expand Down
29 changes: 29 additions & 0 deletions src/entities/SourceFileNode/lib/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,32 @@ export function getLocalIdentifiers(node: SourceFileNode): Set<string> {
visit(node.sourceFile);
return locals;
}

/**
* 특정 파일을 import하고 있는 파일들을 찾음 (dependents)
* @param targetFilePath - 찾으려는 파일 경로
* @param fullNodeMap - 전체 파일 노드 맵
* @param files - 파일 콘텐츠 맵
* @param resolvePath - 경로 해석 함수
* @returns targetFilePath를 import하는 파일 경로 배열
*/
export function getDependents(
targetFilePath: string,
fullNodeMap: Map<string, SourceFileNode>,
files: Record<string, string>,
resolvePath: (from: string, to: string, files: Record<string, string>) => string | null
): string[] {
const dependents: string[] = [];

fullNodeMap.forEach((node) => {
// Symbol 노드는 스킵 (type === 'file'만 처리)
if (node.type !== 'file') return;

const deps = getDependencies(node, files, resolvePath);
if (deps.includes(targetFilePath)) {
dependents.push(node.filePath);
}
});

return dependents;
}
39 changes: 37 additions & 2 deletions src/features/KeyboardShortcuts/KeyboardShortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@
* - 렌더링 없는 로직 전용 컴포넌트
*/

import { useAtomValue, useSetAtom } from 'jotai';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useEffect, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { isSidebarOpenAtom } from '@/app/ui/AppSidebar/model/atoms';
import { viewModeAtom } from '@/entities/AppView/model/atoms';
import { useOpenFile } from '@/features/File/OpenFiles/lib/useOpenFile';
import { searchModalOpenAtom } from '@/features/Search/UnifiedSearch/model/atoms';
import { activeTabIdAtom, openedTabsAtom } from '@/widgets/MainContents/model/atoms';

const GLOBAL_HOTKEYS = {
TOGGLE_SIDEBAR: 'mod+\\',
TOGGLE_VIEW_MODE: 'backquote',
CLOSE_FILE: 'mod+w',
CLOSE_FILE_ESC: 'escape',
CONTENT_SEARCH: 'mod+shift+f',
NEW_SEARCH_TAB: 'mod+shift+tab',
} as const;

export const KeyboardShortcuts = () => {
Expand All @@ -25,6 +28,8 @@ export const KeyboardShortcuts = () => {
const viewMode = useAtomValue(viewModeAtom);
const setViewMode = useSetAtom(viewModeAtom);
const { closeFile } = useOpenFile();
const [openedTabs, setOpenedTabs] = useAtom(openedTabsAtom);
const setActiveTabId = useSetAtom(activeTabIdAtom);

// Global hotkeys (no ref needed - always active)
useHotkeys(
Expand All @@ -47,10 +52,40 @@ export const KeyboardShortcuts = () => {
closeFile();
console.log('[KeyboardShortcuts] Close current file');
break;
case GLOBAL_HOTKEYS.CONTENT_SEARCH:
setViewMode('contentSearch');
console.log('[KeyboardShortcuts] Content search view opened');
break;
case GLOBAL_HOTKEYS.NEW_SEARCH_TAB: {
// 새 Search 탭 열기
const existingSearchTab = openedTabs.find((tab) => tab.type === 'search');

if (existingSearchTab) {
// 이미 Search 탭이 있으면 그 탭으로 전환
setActiveTabId(existingSearchTab.id);
} else {
// 없으면 새로 생성
const newSearchTab = {
id: `search-${Date.now()}`,
type: 'search' as const,
label: 'Search',
};
setOpenedTabs([...openedTabs, newSearchTab]);
setActiveTabId(newSearchTab.id);
}

// viewMode를 TabContainer가 표시되도록 설정
if (viewMode !== 'ide' && viewMode !== 'contentSearch') {
setViewMode('contentSearch');
}

console.log('[KeyboardShortcuts] New search tab opened');
break;
}
}
},
{ enableOnFormTags: true },
[setIsSidebarOpen, setViewMode, viewMode, closeFile]
[setIsSidebarOpen, setViewMode, viewMode, closeFile, openedTabs, setOpenedTabs, setActiveTabId]
);

// Shift+Shift (더블탭) - 검색 모달 열기
Expand Down
Loading