<script setup lang="ts">
import {ref, computed, onBeforeUnmount, watch, nextTick} from "vue";
import { invoke } from "@tauri-apps/api/core";
import SettingsModal from "./SettingsModal.vue";
import TreeEntry from "./TreeEntry.vue";

type SettingsPage = "about" | "editor";
type DocumentMode = "preview" | "edit" | "mixed";
type AppTheme = "system" | "warm" | "white" | "black" | "gruvbox" | "gray";

interface DirEntry {
  name: string;
  path: string;
  is_dir: boolean;
  children?: DirEntry[];
}

interface WorkspaceEntry {
  name: string;
  path: string;
}


const props = withDefaults(
    defineProps<{
      folderPath: string;
      entries: DirEntry[];
      currentFilePath?: string | null;
      settingsRequest?: number;
      settingsInitialPage?: SettingsPage;
      defaultDocumentMode?: DocumentMode;
      appTheme?: AppTheme;
      recentWorkspaces?: WorkspaceEntry[];
    }>(),
    {
      currentFilePath: null,
      settingsRequest: 0,
      settingsInitialPage: "about",
      defaultDocumentMode: "edit",
      appTheme: "warm",
      recentWorkspaces: () => [],
    },
);

const emit = defineEmits<{
  openFolder: [];
  switchWorkspace: [path: string];
  removeWorkspace: [path: string];
  toggleFolder: [path: string];
  openFile: [path: string];
  createFile: [path: string];
  createFolder: [path: string];
  deleteEntry: [path: string];
  renameEntry: [path: string, newName: string];
  updateDefaultDocumentMode: [mode: DocumentMode];
  updateAppTheme: [theme: AppTheme];
  "search-result-click": [filePath: string, lineNumber: number];
}>();

// ---- sidebar view mode ----
type ViewMode = "folder" | "search" | "setting";
const activeView = ref<ViewMode>("folder");

// ---- search types ----
interface SearchMatch {
  line: number;
  column: number;
  line_text: string;
  context_before: string[];
  context_after: string[];
}

interface FileSearchResult {
  relative_path: string;
  absolute_path: string;
  matches: SearchMatch[];
  match_count: number;
}

const SEARCH_HISTORY_KEY = "yurou:search-history";
const MAX_SEARCH_HISTORY = 10;
const SEARCH_DEBOUNCE_MS = 300;

const searchKeyword = ref("");
const searchCaseSensitive = ref(false);
const searchUseRegex = ref(false);
const searchWholeWord = ref(false);
const searchExtensions = ref<string[]>(["md"]);
const searchResults = ref<FileSearchResult[]>([]);
const searchLoading = ref(false);
const searchError = ref("");
const searchHistory = ref<string[]>(loadSearchHistory());
const searchInputRef = ref<HTMLInputElement | null>(null);
const searchFocusedFile = ref<string | null>(null);
let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null;

const searchableTypes = [
  { label: "Markdown (.md)", value: "md" },
  { label: "纯文本 (.txt)", value: "txt" },
  { label: "JSON (.json)", value: "json" },
  { label: "YAML (.yaml/.yml)", value: "yaml" },
  { label: "JavaScript (.js)", value: "js" },
  { label: "TypeScript (.ts)", value: "ts" },
  { label: "CSS (.css)", value: "css" },
  { label: "HTML (.html)", value: "html" },
  { label: "XML (.xml)", value: "xml" },
];

const showExtensionDropdown = ref(false);
const extensionDropdownRef = ref<HTMLElement | null>(null);

const totalMatchCount = computed(() => {
  return searchResults.value.reduce((sum, f) => sum + f.match_count, 0);
});

function loadSearchHistory(): string[] {
  try {
    const stored = localStorage.getItem(SEARCH_HISTORY_KEY);
    return stored ? JSON.parse(stored) : [];
  } catch {
    return [];
  }
}

function saveSearchHistory() {
  try {
    const keyword = searchKeyword.value.trim();
    if (!keyword) return;
    const history = searchHistory.value.filter((h) => h !== keyword);
    history.unshift(keyword);
    const trimmed = history.slice(0, MAX_SEARCH_HISTORY);
    searchHistory.value = trimmed;
    localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(trimmed));
  } catch { /* noop */ }
}

async function performSearch() {
  const keyword = searchKeyword.value.trim();
  if (!keyword || !props.folderPath) {
    searchResults.value = [];
    searchError.value = "";
    return;
  }

  searchLoading.value = true;
  searchError.value = "";

  try {
    const results = await invoke<FileSearchResult[]>("search_files", {
      workspace: props.folderPath,
      keyword,
      caseSensitive: searchCaseSensitive.value,
      useRegex: searchUseRegex.value,
      wholeWord: searchWholeWord.value,
      extensions: searchExtensions.value,
    });
    searchResults.value = results;
    if (results.length === 0) {
      searchError.value = "未找到匹配结果";
    }
  } catch (e) {
    searchError.value = `搜索出错: ${e}`;
    searchResults.value = [];
  } finally {
    searchLoading.value = false;
  }
}

function triggerSearch() {
  if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
  searchDebounceTimer = setTimeout(() => {
    void performSearch();
  }, SEARCH_DEBOUNCE_MS);
}

function clearSearch() {
  searchKeyword.value = "";
  searchResults.value = [];
  searchError.value = "";
  searchFocusedFile.value = null;
}

function toggleExtension(ext: string) {
  const idx = searchExtensions.value.indexOf(ext);
  if (idx >= 0) {
    if (searchExtensions.value.length > 1) {
      searchExtensions.value.splice(idx, 1);
    }
  } else {
    searchExtensions.value = [...searchExtensions.value, ext];
  }
  triggerSearch();
}

function handleSearchKeydown(e: KeyboardEvent) {
  if (e.key === "Escape") {
    clearSearch();
  } else if (e.key === "Enter" && searchResults.value.length > 0) {
    const first = searchResults.value[0];
    if (first.matches.length > 0) {
      saveSearchHistory();
      emit("search-result-click", first.absolute_path, first.matches[0].line);
    }
  }
}

function onResultClick(file: FileSearchResult, match: SearchMatch) {
  saveSearchHistory();
  emit("search-result-click", file.absolute_path, match.line);
}

function toggleFileCollapse(relativePath: string) {
  searchFocusedFile.value = searchFocusedFile.value === relativePath ? null : relativePath;
}

function focusSearchInput() {
  void nextTick(() => {
    searchInputRef.value?.focus();
    searchInputRef.value?.select();
  });
}

function activateSearch() {
  if (activeView.value !== "search") {
    activeView.value = "search";
    if (isCollapsed.value) expand();
  }
  focusSearchInput();
}

function highlightMatchText(text: string): string {
  const keyword = searchKeyword.value.trim();
  if (!keyword || searchUseRegex.value) {
    return escapeHtml(text);
  }
  const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  try {
    const flags = searchCaseSensitive.value ? "g" : "gi";
    const re = new RegExp(`(${escaped})`, flags);
    return escapeHtml(text).replace(re, "<mark class='search-highlight'>$1</mark>");
  } catch {
    return escapeHtml(text);
  }
}

function escapeHtml(text: string): string {
  return text
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

function closeExtensionDropdown(e: MouseEvent) {
  if (extensionDropdownRef.value && !extensionDropdownRef.value.contains(e.target as Node)) {
    showExtensionDropdown.value = false;
  }
}

watch(showExtensionDropdown, (open) => {
  if (open) {
    setTimeout(() => document.addEventListener("click", closeExtensionDropdown), 0);
  } else {
    document.removeEventListener("click", closeExtensionDropdown);
  }
});

watch([searchKeyword, searchCaseSensitive, searchUseRegex, searchWholeWord], () => {
  triggerSearch();
});

// collapsed computed is defined above (line ~71), reuse it
defineExpose({
  activateSearch,
});

// ---- sidebar resize ----
const MIN_SIDEBAR_WIDTH = 180;
const MAX_SIDEBAR_WIDTH = 500;
const COLLAPSED_WIDTH = 48;
const DEFAULT_EXPANDED_WIDTH = 288;

const sidebarWidth = ref(DEFAULT_EXPANDED_WIDTH);
const lastExpandedWidth = ref(DEFAULT_EXPANDED_WIDTH);
const isResizing = ref(false);
const isCollapsed = computed(() => sidebarWidth.value <= COLLAPSED_WIDTH);

function expand() {
  sidebarWidth.value = lastExpandedWidth.value;
}

function collapse() {
  sidebarWidth.value = COLLAPSED_WIDTH;
}

function toggleCollapse() {
  if (isCollapsed.value) {
    expand();
  } else {
    collapse();
  }
}

function switchView(view: ViewMode) {
  if (activeView.value === view) {
    toggleCollapse();
  } else {
    activeView.value = view;
    if (isCollapsed.value) {
      expand();
    }
  }
}

// ---- resize handle drag ----
let resizeStartX = 0;
let resizeStartWidth = 0;

function startResize(e: MouseEvent) {
  resizeStartX = e.clientX;
  resizeStartWidth = sidebarWidth.value;
  isResizing.value = true;
  document.addEventListener("mousemove", onResize);
  document.addEventListener("mouseup", stopResize);
  document.body.style.cursor = "col-resize";
  document.body.style.userSelect = "none";
  e.preventDefault();
}

function onResize(e: MouseEvent) {
  if (!isResizing.value) return;
  const delta = e.clientX - resizeStartX;
  const newWidth = Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, resizeStartWidth + delta));
  sidebarWidth.value = newWidth;
  lastExpandedWidth.value = newWidth;
}

function stopResize() {
  isResizing.value = false;
  document.removeEventListener("mousemove", onResize);
  document.removeEventListener("mouseup", stopResize);
  document.body.style.cursor = "";
  document.body.style.userSelect = "";
}

// ---- workspace switcher ----
const workspaceSwitcherRef = ref<HTMLElement | null>(null);
const showWorkspaceMenu = ref(false);
const showWorkspaceManager = ref(false);
const pendingRemovalWorkspace = ref<WorkspaceEntry | null>(null);

function normalizeWorkspacePath(path: string) {
  const trimmed = path.trim();
  if (!trimmed) return "";

  const normalized = trimmed.replace(/[\\/]+$/, "");
  if (!normalized) return trimmed[0] || "";
  if (/^[A-Za-z]:$/.test(normalized) && /^[A-Za-z]:[\\/]+$/.test(trimmed)) {
    return `${normalized}\\`;
  }

  return normalized;
}

function getWorkspaceIdentity(path: string) {
  return normalizeWorkspacePath(path).toLocaleLowerCase();
}

function getWorkspaceName(path: string) {
  const normalized = normalizeWorkspacePath(path);
  return normalized.split(/[/\\]/).pop() || normalized || path;
}

function getWorkspaceParent(path: string) {
  const normalized = normalizeWorkspacePath(path);
  const parts = normalized.split(/[/\\]/).filter(Boolean);
  if (parts.length <= 1) return normalized;
  return parts.slice(0, -1).join("\\");
}

function isCurrentWorkspace(path: string) {
  return Boolean(props.folderPath) && getWorkspaceIdentity(props.folderPath) === getWorkspaceIdentity(path);
}

const currentWorkspaceName = computed(() => {
  return props.folderPath ? getWorkspaceName(props.folderPath) : "未选择工作目录";
});

const currentWorkspacePath = computed(() => {
  return props.folderPath || "打开或选择一个工作目录";
});

const workspaceMenuItems = computed<WorkspaceEntry[]>(() => {
  const seen = new Set<string>();
  const workspaces: WorkspaceEntry[] = [];

  const addWorkspace = (workspace: WorkspaceEntry) => {
    const normalized = normalizeWorkspacePath(workspace.path);
    if (!normalized) return;

    const identity = getWorkspaceIdentity(normalized);
    if (seen.has(identity)) return;

    seen.add(identity);
    workspaces.push({
      name: workspace.name || getWorkspaceName(normalized),
      path: normalized,
    });
  };

  if (props.folderPath) {
    addWorkspace({
      name: getWorkspaceName(props.folderPath),
      path: props.folderPath,
    });
  }

  props.recentWorkspaces.forEach(addWorkspace);
  return workspaces;
});

const isRemovingCurrentWorkspace = computed(() => {
  return Boolean(
    pendingRemovalWorkspace.value &&
    isCurrentWorkspace(pendingRemovalWorkspace.value.path),
  );
});

function toggleWorkspaceMenu() {
  showWorkspaceMenu.value = !showWorkspaceMenu.value;
}

function closeWorkspaceMenu() {
  showWorkspaceMenu.value = false;
}

function onDocumentClick(event: MouseEvent) {
  const target = event.target;
  if (target instanceof Node && workspaceSwitcherRef.value?.contains(target)) return;
  closeWorkspaceMenu();
}

function openFolderFromMenu() {
  closeWorkspaceMenu();
  emit("openFolder");
}

function selectWorkspace(path: string) {
  closeWorkspaceMenu();
  if (isCurrentWorkspace(path)) return;
  emit("switchWorkspace", path);
}

function openWorkspaceManager() {
  closeWorkspaceMenu();
  showWorkspaceManager.value = true;
}

function closeWorkspaceManager() {
  showWorkspaceManager.value = false;
}

function openFolderFromManager() {
  closeWorkspaceManager();
  emit("openFolder");
}

function selectWorkspaceFromManager(path: string) {
  closeWorkspaceManager();
  if (isCurrentWorkspace(path)) return;
  emit("switchWorkspace", path);
}

function requestRemoveWorkspace(path: string) {
  const normalized = normalizeWorkspacePath(path);
  pendingRemovalWorkspace.value = {
    name: getWorkspaceName(normalized),
    path: normalized,
  };
}

function cancelRemoveWorkspace() {
  pendingRemovalWorkspace.value = null;
}

function confirmRemoveWorkspace() {
  const workspace = pendingRemovalWorkspace.value;
  if (!workspace) return;

  pendingRemovalWorkspace.value = null;
  emit("removeWorkspace", workspace.path);
}

watch(showWorkspaceMenu, (open) => {
  if (open) {
    document.addEventListener("click", onDocumentClick);
  } else {
    document.removeEventListener("click", onDocumentClick);
  }
});

watch(() => props.folderPath, () => {
  closeWorkspaceMenu();
});

onBeforeUnmount(() => {
  document.removeEventListener("click", onDocumentClick);
  document.removeEventListener("mousemove", onResize);
  document.removeEventListener("mouseup", stopResize);
  document.body.style.cursor = "";
  document.body.style.userSelect = "";
});

// ---- settings modal ----
const showSettings = ref(false);
const settingsPage = ref<SettingsPage>("about");

watch(() => props.settingsRequest, (request) => {
  if (request <= 0) return;
  openSettings(props.settingsInitialPage);
});

function openSettings(page: SettingsPage) {
  settingsPage.value = page;
  showSettings.value = true;
}


</script>

<template>
  <aside
      class="directory-sidebar flex flex-row overflow-hidden shrink-0"
      :class="{ 'transition-all duration-300 ease-in-out': !isResizing }"
      :style="{ width: sidebarWidth + 'px' }"
  >
    <!-- ============ Icon bar ============ -->
    <div class="w-12 shrink-0 flex flex-col items-center gap-1 py-3">
      <!-- Folder -->
      <button
          class="sidebar-icon-btn px-2 py-1 cursor-pointer rounded-md transition-colors"
          :class="{ 'sidebar-icon-btn-active': activeView === 'folder' }"
          title="文件"
          @click="switchView('folder')"
      >
        <i class="ri-folder-line text-base"></i>
      </button>
      <!-- Search -->
      <button
          class="sidebar-icon-btn px-2 py-1 cursor-pointer rounded-md transition-colors"
          :class="{ 'sidebar-icon-btn-active': activeView === 'search' }"
          title="搜索"
          @click="switchView('search')"
      >
        <i class="ri-search-line text-base"></i>
      </button>
      <!-- ===== Settings ===== -->
      <button
          class="sidebar-icon-btn px-2 py-1 cursor-pointer rounded-md transition-colors mt-auto"
          :class="{ 'sidebar-icon-btn-active': showSettings }"
          title="设置"
          @click="openSettings('about')"
      >
        <i class="ri-settings-line text-sm"></i>
      </button>
    </div>

    <!-- ============ Expanded panel ============ -->
    <div v-show="!isCollapsed" class="directory-sidebar-panel flex-1 flex flex-col overflow-hidden">
      <!-- ===== FOLDER VIEW ===== -->
      <div v-if="activeView === 'folder'" class="flex flex-col flex-1 overflow-hidden">
        <!-- File tree -->
        <div
            class="flex-1 overflow-y-auto px-2 py-2 flex flex-col gap-0.5"
            
        >
          <TreeEntry
              v-for="entry in entries"
              :key="entry.path"
              :entry="entry"
              :depth="0"
              :currentFilePath="currentFilePath"
              @toggle-folder="(path: string) => emit('toggleFolder', path)"
              @open-file="(path: string) => emit('openFile', path)"
              @create-folder="(path: string) => emit('createFolder', path)"
              @create-file="(path: string) => emit('createFile', path)"
              @delete-entry="(path: string) => emit('deleteEntry', path)"
              @rename-entry="(path: string, name: string) => emit('renameEntry', path, name)"
          />

          <!-- Empty state -->
          <div
              v-if="!folderPath"
              class="sidebar-empty px-2 py-4 text-xs text-center"
          >
            <p class="mb-3">暂无打开的文件夹</p>
            <button
                class="sidebar-empty-btn inline-flex items-center gap-1.5 px-3 py-1.5 text-xs cursor-pointer rounded-md transition-colors"
                @click="$emit('openFolder')"
            >
              <i class="ri-folder-open-line"></i>
              打开文件夹
            </button>
          </div>
        </div>
      </div>

      <!-- ===== SEARCH VIEW ===== -->
      <div v-if="activeView === 'search'" class="flex flex-col flex-1 overflow-hidden">
        <!-- 搜索输入区 -->
        <div class="px-3 py-3">
          <div class="relative">
            <i class="sidebar-search-icon ri-search-line absolute left-2.5 top-1/2 -translate-y-1/2 text-sm"></i>
            <input
                ref="searchInputRef"
                v-model="searchKeyword"
                class="sidebar-search-input w-full pl-8 pr-8 py-1.5 text-xs rounded-md outline-none"
                placeholder="搜索文件内容..."
                @keydown="handleSearchKeydown"
            />
            <button
                v-if="searchKeyword"
                class="absolute right-1.5 top-1/2 -translate-y-1/2 p-0.5 rounded text-xs cursor-pointer hover:bg-black/10 dark:hover:bg-white/10 text-[var(--app-subtle)]"
                title="清空"
                @click="clearSearch"
            >
              <i class="ri-close-line"></i>
            </button>
          </div>

          <!-- 搜索选项 -->
          <div class="flex items-center gap-2 mt-2 flex-wrap">
            <button
                class="search-option-btn"
                :class="{ 'search-option-active': searchCaseSensitive }"
                title="区分大小写"
                @click="searchCaseSensitive = !searchCaseSensitive"
            >
              Aa
            </button>
            <button
                class="search-option-btn"
                :class="{ 'search-option-active': searchUseRegex }"
                title="正则表达式"
                @click="searchUseRegex = !searchUseRegex"
            >
              .*
            </button>
            <button
                class="search-option-btn"
                :class="{ 'search-option-active': searchWholeWord }"
                title="全词匹配"
                @click="searchWholeWord = !searchWholeWord"
            >
              <i class="ri-font-size"></i>
            </button>

            <!-- 文件类型下拉 -->
            <div ref="extensionDropdownRef" class="relative">
              <button
                  class="search-option-btn flex items-center gap-1"
                  @click="showExtensionDropdown = !showExtensionDropdown"
                  title="文件类型"
              >
                <span class="text-[10px]">.{{ searchExtensions.join(', .') }}</span>
                <i class="ri-arrow-down-s-line text-[9px]"></i>
              </button>
              <div
                  v-if="showExtensionDropdown"
                  class="search-ext-dropdown absolute top-full left-0 mt-1 bg-[var(--app-surface)] border border-[var(--app-border)] rounded-md shadow-lg z-30 py-1 min-w-[150px] max-h-52 overflow-y-auto"
                  @click.stop
              >
                <label
                    v-for="type in searchableTypes"
                    :key="type.value"
                    class="flex items-center gap-2 px-3 py-1.5 text-xs cursor-pointer hover:bg-[var(--app-hover)]"
                >
                  <input
                      type="checkbox"
                      :checked="searchExtensions.includes(type.value)"
                      class="w-3 h-3 accent-[var(--app-accent)]"
                      @change="toggleExtension(type.value)"
                  />
                  {{ type.label }}
                </label>
              </div>
            </div>
          </div>
        </div>

        <!-- 搜索结果区域 -->
        <div class="flex-1 overflow-y-auto px-2 pb-2">
          <!-- Loading -->
          <div v-if="searchLoading" class="flex items-center justify-center py-8">
            <p class="text-xs text-[var(--app-subtle)]">搜索中...</p>
          </div>

          <!-- Error / No results -->
          <div v-else-if="searchError && searchResults.length === 0" class="flex items-center justify-center py-8">
            <p class="text-xs text-[var(--app-subtle)]">{{ searchError }}</p>
          </div>

          <!-- Empty prompt / history -->
          <div v-else-if="!searchKeyword.trim()" class="flex flex-col py-4 px-1">
            <template v-if="searchHistory.length > 0">
              <p class="text-[10px] text-[var(--app-subtle)] px-2 mb-2 uppercase tracking-wider">最近搜索</p>
              <button
                  v-for="(item, idx) in searchHistory.slice(0, 5)"
                  :key="idx"
                  class="search-history-item flex items-center gap-1.5 text-xs px-2 py-1.5 rounded cursor-pointer hover:bg-[var(--app-hover)] w-full text-left truncate"
                  @click="searchKeyword = item"
              >
                <i class="ri-history-line text-[var(--app-subtle)] shrink-0"></i>
                <span class="truncate text-[var(--app-text-soft)]">{{ item }}</span>
              </button>
            </template>
            <div v-else class="flex flex-col items-center justify-center py-8">
              <p v-if="props.folderPath" class="text-xs text-[var(--app-subtle)]">
                输入关键词搜索工作目录中的文件
              </p>
              <p v-else class="text-xs text-[var(--app-subtle)]">
                请先打开一个工作目录
              </p>
            </div>
          </div>

          <!-- Results list (grouped by file) -->
          <div v-else class="flex flex-col gap-1">
            <div class="text-[10px] text-[var(--app-subtle)] px-1 py-1">
              {{ searchResults.length }} 个文件，共 {{ totalMatchCount }} 处匹配
            </div>
            <div
                v-for="file in searchResults"
                :key="file.absolute_path"
                class="search-file-group"
            >
              <!-- File header -->
              <button
                  class="search-file-header w-full flex items-center gap-1.5 px-2 py-1.5 text-xs rounded cursor-pointer hover:bg-[var(--app-hover)] transition-colors"
                  @click="toggleFileCollapse(file.relative_path)"
              >
                <i
                    class="text-[10px] transition-transform"
                    :class="searchFocusedFile === file.relative_path ? 'ri-arrow-down-s-line' : 'ri-arrow-right-s-line'"
                ></i>
                <i class="ri-file-text-line text-[var(--app-subtle)] shrink-0"></i>
                <span class="truncate flex-1 text-left text-[var(--app-text-soft)]">{{ file.relative_path }}</span>
                <span class="text-[var(--app-subtle)] shrink-0 text-[10px]">{{ file.match_count }}</span>
              </button>

              <!-- File matches (collapsible) -->
              <div
                  v-show="searchFocusedFile !== file.relative_path"
                  class="search-matches ml-5"
              >
                <button
                    v-for="(match, mIdx) in file.matches"
                    :key="mIdx"
                    class="search-match-item w-full text-left px-2 py-1.5 text-xs rounded cursor-pointer hover:bg-[var(--app-hover)] transition-colors block"
                    :class="{ 'bg-[var(--app-active-bg)]': props.currentFilePath === file.absolute_path }"
                    @click="onResultClick(file, match)"
                >
                  <div class="flex items-baseline gap-2 mb-0.5">
                    <span class="text-[10px] text-[var(--app-accent)] shrink-0 font-mono">L{{ match.line }}</span>
                    <span class="truncate text-[var(--app-text-soft)]" v-html="highlightMatchText(match.line_text)"></span>
                  </div>
                  <!-- Context lines -->
                  <div v-if="match.context_before.length || match.context_after.length" class="text-[10px] text-[var(--app-subtle)] mt-0.5 space-y-0">
                    <div v-for="(ctxLine, ci) in match.context_before" :key="'b' + ci" class="truncate opacity-60">
                      {{ ctxLine }}
                    </div>
                    <div v-for="(ctxLine, ci) in match.context_after" :key="'a' + ci" class="truncate opacity-60">
                      {{ ctxLine }}
                    </div>
                  </div>
                </button>
                <div v-if="file.match_count > file.matches.length" class="text-[10px] text-[var(--app-subtle)] px-2 py-0.5">
                  ... 还有 {{ file.match_count - file.matches.length }} 处匹配未显示
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>

      <!-- ===== WORKSPACE SWITCHER ===== -->
      <div ref="workspaceSwitcherRef" class="workspace-switcher">
        <Transition name="workspace-menu-pop">
          <div
              v-if="showWorkspaceMenu"
              class="workspace-menu"
              role="menu"
              aria-label="工作目录"
              @click.stop
          >
            <div v-if="workspaceMenuItems.length" class="workspace-menu-list">
              <button
                  v-for="workspace in workspaceMenuItems"
                  :key="workspace.path"
                  class="workspace-menu-item"
                  :class="{ 'workspace-menu-item-active': isCurrentWorkspace(workspace.path) }"
                  type="button"
                  role="menuitem"
                  :title="workspace.path"
                  @click="selectWorkspace(workspace.path)"
              >
                <span class="workspace-menu-item-copy">
                  <span class="workspace-menu-item-name">{{ workspace.name }}</span>
                  <span class="workspace-menu-item-path">{{ getWorkspaceParent(workspace.path) }}</span>
                </span>
                <i v-if="isCurrentWorkspace(workspace.path)" class="ri-check-line workspace-menu-check"></i>
              </button>
            </div>
            <div v-else class="workspace-menu-empty">
              暂无工作目录
            </div>

            <div class="workspace-menu-separator"></div>

            <button
                class="workspace-menu-command"
                type="button"
                role="menuitem"
                @click="openFolderFromMenu"
            >
              <i class="ri-folder-open-line"></i>
              <span>打开工作目录</span>
            </button>
            <button
                class="workspace-menu-command"
                type="button"
                role="menuitem"
                @click="openWorkspaceManager"
            >
              <i class="ri-book-shelf-line"></i>
              <span>管理工作目录</span>
            </button>
          </div>
        </Transition>

        <button
            class="workspace-trigger"
            type="button"
            :title="currentWorkspacePath"
            aria-haspopup="menu"
            :aria-expanded="showWorkspaceMenu"
            @click.stop="toggleWorkspaceMenu"
        >
          <i class="ri-arrow-up-down-line workspace-trigger-icon"></i>
          <span class="workspace-trigger-name">{{ currentWorkspaceName }}</span>
          <i class="ri-arrow-down-s-line workspace-trigger-caret"></i>
        </button>
      </div>
    </div>

    <!-- ============ Resize handle ============ -->
    <div
        v-show="!isCollapsed"
        class="sidebar-resize-handle"
        :class="{ 'sidebar-resize-handle-active': isResizing }"
        @mousedown="startResize"
    >
      <div class="sidebar-resize-handle-bar"></div>
    </div>
  </aside>

  <SettingsModal
      v-model:show="showSettings"
      :initial-page="settingsPage"
      :default-document-mode="defaultDocumentMode"
      :app-theme="appTheme"
      @update-default-document-mode="(mode: DocumentMode) => emit('updateDefaultDocumentMode', mode)"
      @update-app-theme="(theme: AppTheme) => emit('updateAppTheme', theme)"
  />

  <Teleport to="body">
    <div
        v-if="showWorkspaceManager"
        class="workspace-manager-overlay"
        role="presentation"
        @click.self="closeWorkspaceManager"
    >
      <section
          class="workspace-manager-shell"
          role="dialog"
          aria-modal="true"
          aria-label="管理工作目录"
      >
        <header class="workspace-manager-header">
          <div class="workspace-manager-title">
            <h2>管理工作目录</h2>
            <p>{{ currentWorkspaceName }}</p>
          </div>
          <button
              class="workspace-manager-close"
              type="button"
              title="关闭"
              aria-label="关闭"
              @click="closeWorkspaceManager"
          >
            <i class="ri-close-line"></i>
          </button>
        </header>

        <div class="workspace-manager-body">
          <div class="workspace-manager-list" aria-label="最近工作目录">
            <div v-if="!workspaceMenuItems.length" class="workspace-manager-empty">
              暂无工作目录
            </div>

            <div
                v-for="workspace in workspaceMenuItems"
                :key="workspace.path"
                class="workspace-manager-row"
                :class="{ 'workspace-manager-row-current': isCurrentWorkspace(workspace.path) }"
            >
              <div class="workspace-manager-row-main">
                <i class="ri-folder-line"></i>
                <div class="workspace-manager-row-copy">
                  <strong>{{ workspace.name }}</strong>
                  <span>{{ workspace.path }}</span>
                </div>
              </div>
              <div class="workspace-manager-row-actions">
                <span v-if="isCurrentWorkspace(workspace.path)" class="workspace-manager-current-label">
                  当前
                </span>
                <button
                    v-else
                    class="workspace-manager-icon-btn"
                    type="button"
                    title="打开"
                    aria-label="打开"
                    @click="selectWorkspaceFromManager(workspace.path)"
                >
                  <i class="ri-arrow-right-line"></i>
                </button>
                <button
                    class="workspace-manager-icon-btn"
                    type="button"
                    title="移除"
                    aria-label="移除"
                    @click="requestRemoveWorkspace(workspace.path)"
                >
                  <i class="ri-delete-bin-line"></i>
                </button>
              </div>
            </div>
          </div>

          <footer class="workspace-manager-actions">
            <button
                class="workspace-manager-primary"
                type="button"
                @click="openFolderFromManager"
            >
              <i class="ri-folder-open-line"></i>
              <span>打开本地工作目录</span>
            </button>
          </footer>
        </div>
      </section>
    </div>

    <div
        v-if="pendingRemovalWorkspace"
        class="workspace-remove-confirm-overlay"
        role="presentation"
        @click.self="cancelRemoveWorkspace"
    >
      <section
          class="workspace-remove-confirm-shell"
          role="dialog"
          aria-modal="true"
          aria-labelledby="workspace-remove-confirm-title"
      >
        <header class="workspace-remove-confirm-header">
          <i class="ri-error-warning-line"></i>
          <h3 id="workspace-remove-confirm-title">移除工作目录</h3>
        </header>
        <div class="workspace-remove-confirm-body">
          <p>
            确定要从列表中移除“{{ pendingRemovalWorkspace.name }}”吗？
          </p>
          <p>
            这只会移除工作目录记录，不会删除本地文件。
          </p>
          <p v-if="isRemovingCurrentWorkspace">
            当前工作目录会被关闭，编辑区域会被重置。
          </p>
        </div>
        <footer class="workspace-remove-confirm-actions">
          <button
              class="workspace-remove-confirm-cancel"
              type="button"
              @click="cancelRemoveWorkspace"
          >
            取消
          </button>
          <button
              class="workspace-remove-confirm-danger"
              type="button"
              @click="confirmRemoveWorkspace"
          >
            移除
          </button>
        </footer>
      </section>
    </div>
  </Teleport>
</template>

<style scoped>
.directory-sidebar {
  position: relative;
  border-right: 1px solid var(--app-border);
  background: color-mix(in srgb, var(--app-sidebar-bg) 82%, transparent);
}

.directory-sidebar-panel {
  border-left: 1px solid var(--app-border);
}

/* ---- Resize handle ---- */
.sidebar-resize-handle {
  position: absolute;
  top: 0;
  right: -2px;
  bottom: 0;
  z-index: 25;
  display: flex;
  width: 7px;
  align-items: center;
  justify-content: center;
  cursor: col-resize;
  transition: background-color 0.15s ease;
}

.sidebar-resize-handle:hover,
.sidebar-resize-handle-active {
  background: color-mix(in srgb, var(--app-accent) 18%, transparent);
}

.sidebar-resize-handle-bar {
  width: 1px;
  height: 100%;
  border-radius: 1px;
  background: transparent;
  transition: background-color 0.15s ease;
}

.sidebar-resize-handle:hover .sidebar-resize-handle-bar,
.sidebar-resize-handle-active .sidebar-resize-handle-bar {
  background: color-mix(in srgb, var(--app-accent) 36%, transparent);
}

.sidebar-icon-btn {
  color: var(--app-subtle);
}

.sidebar-icon-btn:hover,
.sidebar-icon-btn-active {
  background: var(--app-hover);
  color: var(--app-accent);
}

.sidebar-empty,
.sidebar-search-icon {
  color: var(--app-subtle);
}

.sidebar-empty-btn {
  border: 1px solid color-mix(in srgb, var(--app-accent-muted) 42%, transparent);
  background: var(--app-active-bg);
  color: var(--app-accent);
}

.sidebar-empty-btn:hover {
  background: color-mix(in srgb, var(--app-active-bg) 84%, var(--app-surface));
  color: var(--app-accent);
}

.sidebar-search-input {
  border: 1px solid var(--app-border-strong);
  background: var(--app-hover);
  color: var(--app-text);
}

.sidebar-search-input:focus {
  border-color: var(--app-accent);
}

.sidebar-search-input::placeholder {
  color: var(--app-subtle);
}

.workspace-switcher {
  position: relative;
  z-index: 20;
  padding: 6px;
  border-top: 1px solid var(--app-border);
  background: color-mix(in srgb, var(--app-sidebar-bg) 88%, transparent);
}

.workspace-trigger {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr) 16px;
  align-items: center;
  gap: 6px;
  width: 100%;
  height: 32px;
  padding: 0 8px;
  border: 1px solid transparent;
  border-radius: 7px;
  background: transparent;
  color: var(--app-text-soft);
  cursor: pointer;
  font-size: 12px;
  line-height: 1;
  text-align: left;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease,
    color 0.15s ease;
}

.workspace-trigger:hover,
.workspace-trigger[aria-expanded="true"] {
  border-color: var(--app-border-strong);
  background: var(--app-hover);
  color: var(--app-text);
}

.workspace-trigger-icon,
.workspace-trigger-caret {
  color: var(--app-subtle);
  font-size: 14px;
}

.workspace-trigger-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}

.workspace-menu-pop-enter-active,
.workspace-menu-pop-leave-active {
  transition:
    opacity 0.12s ease,
    transform 0.12s ease;
}

.workspace-menu-pop-enter-from,
.workspace-menu-pop-leave-to {
  opacity: 0;
  transform: translateY(4px);
}

.workspace-menu {
  position: absolute;
  right: 6px;
  bottom: calc(100% + 6px);
  left: 6px;
  overflow: hidden;
  border: 1px solid var(--app-border-strong);
  border-radius: 8px;
  background: var(--app-surface);
  box-shadow:
    0 14px 34px color-mix(in srgb, var(--app-shadow) 70%, transparent),
    0 1px 4px color-mix(in srgb, var(--app-shadow) 70%, transparent);
}

.workspace-menu-list {
  max-height: 210px;
  overflow-y: auto;
  padding: 4px;
}

.workspace-menu-item,
.workspace-menu-command {
  display: flex;
  width: 100%;
  align-items: center;
  border: none;
  background: transparent;
  color: var(--app-text-soft);
  cursor: pointer;
  text-align: left;
}

.workspace-menu-item {
  min-height: 42px;
  gap: 8px;
  justify-content: space-between;
  padding: 6px 8px;
  border-radius: 6px;
}

.workspace-menu-item:hover,
.workspace-menu-command:hover {
  background: var(--app-hover);
  color: var(--app-text);
}

.workspace-menu-item-active {
  color: var(--app-text-strong);
  font-weight: 600;
}

.workspace-menu-item-copy {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 3px;
}

.workspace-menu-item-name,
.workspace-menu-item-path {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.workspace-menu-item-name {
  font-size: 13px;
  line-height: 1.1;
}

.workspace-menu-item-path {
  color: var(--app-subtle);
  font-size: 11px;
  font-weight: 400;
  line-height: 1.1;
}

.workspace-menu-check {
  flex-shrink: 0;
  color: var(--app-accent);
  font-size: 15px;
}

.workspace-menu-empty {
  padding: 12px 10px 10px;
  color: var(--app-subtle);
  font-size: 12px;
  text-align: center;
}

.workspace-menu-separator {
  height: 1px;
  background: var(--app-border);
}

.workspace-menu-command {
  gap: 8px;
  min-height: 34px;
  padding: 0 10px;
  font-size: 13px;
}

.workspace-menu-command i {
  color: var(--app-muted);
  font-size: 15px;
}


.workspace-manager-overlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 28px;
  background: var(--app-overlay);
  backdrop-filter: blur(8px);
}

.workspace-manager-shell {
  display: flex;
  width: min(720px, calc(100vw - 48px));
  max-height: min(620px, calc(100vh - 48px));
  min-height: 390px;
  overflow: hidden;
  flex-direction: column;
  border: 1px solid var(--app-border-strong);
  border-radius: 12px;
  background: var(--app-surface);
  box-shadow:
    0 24px 70px var(--app-shadow),
    0 2px 10px color-mix(in srgb, var(--app-shadow) 46%, transparent);
}

.workspace-manager-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 66px;
  padding: 0 18px 0 22px;
  border-bottom: 1px solid var(--app-border);
  background: color-mix(in srgb, var(--app-surface) 72%, transparent);
}

.workspace-manager-title {
  min-width: 0;
}

.workspace-manager-title h2 {
  margin: 0 0 5px;
  color: var(--app-text-strong);
  font-size: 16px;
  font-weight: 700;
  line-height: 1.1;
}

.workspace-manager-title p {
  margin: 0;
  overflow: hidden;
  color: var(--app-muted);
  font-size: 12px;
  line-height: 1.2;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.workspace-manager-close,
.workspace-manager-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  color: var(--app-muted);
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.workspace-manager-close {
  width: 30px;
  height: 30px;
  border-radius: 6px;
  font-size: 17px;
}

.workspace-manager-close:hover,
.workspace-manager-icon-btn:hover:not(:disabled) {
  background: var(--app-hover);
  color: var(--app-text);
}

.workspace-manager-body {
  display: flex;
  min-height: 0;
  flex: 1;
  flex-direction: column;
  background: var(--app-panel-bg);
}

.workspace-manager-list {
  min-height: 0;
  flex: 1;
  overflow-y: auto;
  padding: 14px;
}

.workspace-manager-empty {
  display: flex;
  min-height: 190px;
  align-items: center;
  justify-content: center;
  color: var(--app-subtle);
  font-size: 13px;
}

.workspace-manager-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 14px;
  align-items: center;
  min-height: 62px;
  padding: 10px 10px 10px 12px;
  border-radius: 8px;
  color: var(--app-text-soft);
}

.workspace-manager-row + .workspace-manager-row {
  margin-top: 4px;
}

.workspace-manager-row:hover {
  background: var(--app-hover);
}

.workspace-manager-row-current {
  background: var(--app-active-bg);
  color: var(--app-text);
}

.workspace-manager-row-main {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 11px;
}

.workspace-manager-row-main > i {
  flex-shrink: 0;
  color: color-mix(in srgb, var(--app-accent) 76%, transparent);
  font-size: 18px;
}

.workspace-manager-row-copy {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: 5px;
}

.workspace-manager-row-copy strong,
.workspace-manager-row-copy span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.workspace-manager-row-copy strong {
  color: var(--app-text-strong);
  font-size: 14px;
  font-weight: 700;
}

.workspace-manager-row-copy span {
  color: var(--app-muted);
  font-size: 12px;
}

.workspace-manager-row-actions {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.workspace-manager-current-label {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 0 8px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--app-accent) 12%, transparent);
  color: var(--app-accent);
  font-size: 12px;
  font-weight: 600;
}

.workspace-manager-icon-btn {
  width: 30px;
  height: 30px;
  border-radius: 6px;
  font-size: 15px;
}

.workspace-manager-icon-btn:disabled {
  cursor: default;
  opacity: 0.34;
}

.workspace-manager-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 12px;
  min-height: 62px;
  padding: 12px 16px;
  border-top: 1px solid var(--app-border);
  background: var(--app-surface);
}

.workspace-manager-primary {
  display: inline-flex;
  gap: 8px;
  min-height: 32px;
  min-width: 142px;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 6px;
  background: var(--app-control);
  color: var(--app-control-contrast);
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  box-shadow: 0 2px 7px color-mix(in srgb, var(--app-control) 32%, transparent);
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease,
    box-shadow 0.15s ease;
}

.workspace-manager-primary:hover {
  background: var(--app-control-hover);
}

.workspace-remove-confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: color-mix(in srgb, var(--app-overlay) 86%, transparent);
  backdrop-filter: blur(10px);
}

.workspace-remove-confirm-shell {
  width: min(420px, calc(100vw - 40px));
  overflow: hidden;
  border: 1px solid var(--app-border-strong);
  border-radius: 10px;
  background: var(--app-surface);
  box-shadow:
    0 22px 56px var(--app-shadow),
    0 2px 10px color-mix(in srgb, var(--app-shadow) 45%, transparent);
}

.workspace-remove-confirm-header {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 56px;
  padding: 0 18px;
  border-bottom: 1px solid var(--app-border);
}

.workspace-remove-confirm-header i {
  color: #d97706;
  font-size: 19px;
}

.workspace-remove-confirm-header h3 {
  margin: 0;
  color: var(--app-text-strong);
  font-size: 15px;
  font-weight: 700;
  line-height: 1.2;
}

.workspace-remove-confirm-body {
  padding: 16px 18px 4px;
}

.workspace-remove-confirm-body p {
  margin: 0 0 10px;
  color: var(--app-text-soft);
  font-size: 13px;
  line-height: 1.55;
}

.workspace-remove-confirm-body p:first-child {
  color: var(--app-text);
}

.workspace-remove-confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  padding: 14px 18px 16px;
}

.workspace-remove-confirm-cancel,
.workspace-remove-confirm-danger {
  display: inline-flex;
  min-width: 74px;
  min-height: 32px;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease,
    color 0.15s ease;
}

.workspace-remove-confirm-cancel {
  border: 1px solid var(--app-border-strong);
  background: var(--app-surface);
  color: var(--app-text-soft);
}

.workspace-remove-confirm-cancel:hover {
  background: var(--app-hover);
  color: var(--app-text);
}

.workspace-remove-confirm-danger {
  border: 1px solid #dc2626;
  background: #dc2626;
  color: #ffffff;
}

.workspace-remove-confirm-danger:hover {
  border-color: #b91c1c;
  background: #b91c1c;
}

@media (max-width: 680px) {
  .workspace-manager-overlay {
    align-items: stretch;
    padding: 14px;
  }

  .workspace-manager-shell {
    width: 100%;
    max-height: none;
  }

  .workspace-manager-row {
    grid-template-columns: 1fr;
    gap: 8px;
  }

  .workspace-manager-row-actions {
    justify-content: flex-end;
  }

  .workspace-manager-actions {
    align-items: stretch;
    flex-direction: column;
  }

  .workspace-manager-primary {
    width: 100%;
  }

  .workspace-remove-confirm-overlay {
    align-items: flex-end;
    padding: 14px;
  }

  .workspace-remove-confirm-shell {
    width: 100%;
  }
}

/* ── Search view ──────────────────────────────────────────────── */

.search-option-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 24px;
  min-width: 24px;
  padding: 0 6px;
  font-size: 11px;
  font-weight: 500;
  border-radius: 4px;
  border: 1px solid transparent;
  background: transparent;
  color: var(--app-subtle);
  cursor: pointer;
  transition: all 0.15s;
}

.search-option-btn:hover {
  background: var(--app-hover);
  color: var(--app-text-soft);
}

.search-option-active {
  background: color-mix(in srgb, var(--app-accent) 15%, transparent);
  color: var(--app-accent);
  border-color: color-mix(in srgb, var(--app-accent) 30%, transparent);
}

.search-file-header {
  color: var(--app-text-soft);
}

.search-match-item {
  border-left: 2px solid transparent;
}

.search-match-item:hover {
  border-left-color: var(--app-accent-muted);
}

.search-highlight {
  background: color-mix(in srgb, var(--app-accent) 30%, transparent);
  color: var(--app-accent);
  border-radius: 2px;
  padding: 0 1px;
}

.search-history-item {
  color: var(--app-text-soft);
}

.search-ext-dropdown {
  backdrop-filter: blur(12px);
}
</style>
