11<script lang="ts" setup>
22import ViewFileTreeItem from ' ./ViewFileTreeItem.vue' ;
3- import {onMounted , useTemplateRef } from ' vue' ;
3+ import {onMounted , onUnmounted , useTemplateRef , ref , computed } from ' vue' ;
44import {createViewFileTreeStore } from ' ./ViewFileTreeStore.ts' ;
5+ import {GET } from ' ../modules/fetch.ts' ;
6+ import {filterRepoFilesWeighted } from ' ../features/repo-findfile.ts' ;
7+ import {pathEscapeSegments } from ' ../utils/url.ts' ;
58
69const elRoot = useTemplateRef (' elRoot' );
10+ const searchQuery = ref (' ' );
11+ const allFiles = ref <string []>([]);
12+ const selectedIndex = ref (0 );
713
814const props = defineProps ({
915 repoLink: {type: String , required: true },
@@ -12,19 +18,106 @@ const props = defineProps({
1218});
1319
1420const store = createViewFileTreeStore (props );
21+
22+ const filteredFiles = computed (() => {
23+ if (! searchQuery .value ) return [];
24+ return filterRepoFilesWeighted (allFiles .value , searchQuery .value );
25+ });
26+
27+ const treeLink = computed (() => ` ${props .repoLink }/src/${props .currentRefNameSubURL } ` );
28+
29+ let searchInputElement: HTMLInputElement | null = null ;
30+
31+ const handleSearchInput = (e : Event ) => {
32+ searchQuery .value = (e .target as HTMLInputElement ).value ;
33+ selectedIndex .value = 0 ;
34+ };
35+
36+ const handleKeyDown = (e : KeyboardEvent ) => {
37+ if (! searchQuery .value || filteredFiles .value .length === 0 ) return ;
38+
39+ if (e .key === ' ArrowDown' ) {
40+ e .preventDefault ();
41+ selectedIndex .value = Math .min (selectedIndex .value + 1 , filteredFiles .value .length - 1 );
42+ } else if (e .key === ' ArrowUp' ) {
43+ e .preventDefault ();
44+ selectedIndex .value = Math .max (selectedIndex .value - 1 , 0 );
45+ } else if (e .key === ' Enter' ) {
46+ e .preventDefault ();
47+ const selectedFile = filteredFiles .value [selectedIndex .value ];
48+ if (selectedFile ) {
49+ handleSearchResultClick (selectedFile .matchResult .join (' ' ));
50+ }
51+ } else if (e .key === ' Escape' ) {
52+ searchQuery .value = ' ' ;
53+ if (searchInputElement ) searchInputElement .value = ' ' ;
54+ }
55+ };
56+
1557onMounted (async () => {
1658 store .rootFiles = await store .loadChildren (' ' , props .treePath );
1759 elRoot .value .closest (' .is-loading' )?.classList ?.remove (' is-loading' );
60+
61+ // Load all files for search
62+ const treeListUrl = elRoot .value .closest (' #view-file-tree' )?.getAttribute (' data-tree-list-url' );
63+ if (treeListUrl ) {
64+ const response = await GET (treeListUrl );
65+ allFiles .value = await response .json ();
66+ }
67+
68+ // Setup search input listener
69+ searchInputElement = document .querySelector (' #file-tree-search' );
70+ if (searchInputElement ) {
71+ searchInputElement .addEventListener (' input' , handleSearchInput );
72+ searchInputElement .addEventListener (' keydown' , handleKeyDown );
73+ }
74+
1875 window .addEventListener (' popstate' , (e ) => {
1976 store .selectedItem = e .state ?.treePath || ' ' ;
2077 if (e .state ?.url ) store .loadViewContent (e .state .url );
2178 });
2279});
80+
81+ onUnmounted (() => {
82+ if (searchInputElement ) {
83+ searchInputElement .removeEventListener (' input' , handleSearchInput );
84+ searchInputElement .removeEventListener (' keydown' , handleKeyDown );
85+ }
86+ });
87+
88+ function handleSearchResultClick(filePath : string ) {
89+ searchQuery .value = ' ' ;
90+ if (searchInputElement ) searchInputElement .value = ' ' ;
91+ window .location .href = ` ${treeLink .value }/${pathEscapeSegments (filePath )} ` ;
92+ }
2393 </script >
2494
2595<template >
26- <div class =" view-file-tree-items" ref =" elRoot" >
27- <ViewFileTreeItem v-for =" item in store.rootFiles" :key =" item.name" :item =" item" :store =" store" />
96+ <div ref =" elRoot" >
97+ <div v-if =" searchQuery && filteredFiles.length > 0" class =" file-tree-search-results" >
98+ <div
99+ v-for =" (result, idx) in filteredFiles"
100+ :key =" result.matchResult.join('')"
101+ :class =" ['file-tree-search-result-item', {'selected': idx === selectedIndex}]"
102+ @click =" handleSearchResultClick(result.matchResult.join(''))"
103+ @mouseenter =" selectedIndex = idx"
104+ >
105+ <svg class =" svg octicon-file" width =" 16" height =" 16" aria-hidden =" true" ><use href =" #octicon-file" /></svg >
106+ <span class =" file-tree-search-result-path" >
107+ <span
108+ v-for =" (part, index) in result.matchResult"
109+ :key =" index"
110+ :class =" {'search-match': index % 2 === 1}"
111+ >{{ part }}</span >
112+ </span >
113+ </div >
114+ </div >
115+ <div v-else-if =" searchQuery && filteredFiles.length === 0" class =" file-tree-search-no-results" >
116+ No matching file found
117+ </div >
118+ <div v-else class =" view-file-tree-items" >
119+ <ViewFileTreeItem v-for =" item in store.rootFiles" :key =" item.name" :item =" item" :store =" store" />
120+ </div >
28121 </div >
29122</template >
30123
@@ -35,4 +128,55 @@ onMounted(async () => {
35128 gap : 1px ;
36129 margin-right : .5rem ;
37130}
131+
132+ .file-tree-search-results {
133+ display : flex ;
134+ flex-direction : column ;
135+ margin : 0 0.5rem 0.5rem ;
136+ max-height : 400px ;
137+ overflow-y : auto ;
138+ background : var (--color-box-body );
139+ border : 1px solid var (--color-secondary );
140+ border-radius : 6px ;
141+ box-shadow : 0 8px 24px rgba (0 , 0 , 0 , 0.12 );
142+ }
143+
144+ .file-tree-search-result-item {
145+ display : flex ;
146+ align-items : center ;
147+ gap : 0.5rem ;
148+ padding : 0.5rem 0.75rem ;
149+ cursor : pointer ;
150+ transition : background-color 0.1s ;
151+ border-bottom : 1px solid var (--color-secondary );
152+ }
153+
154+ .file-tree-search-result-item :last-child {
155+ border-bottom : none ;
156+ }
157+
158+ .file-tree-search-result-item :hover ,
159+ .file-tree-search-result-item.selected {
160+ background-color : var (--color-hover );
161+ }
162+
163+ .file-tree-search-result-path {
164+ flex : 1 ;
165+ overflow : hidden ;
166+ text-overflow : ellipsis ;
167+ white-space : nowrap ;
168+ font-size : 14px ;
169+ }
170+
171+ .search-match {
172+ color : var (--color-red );
173+ font-weight : var (--font-weight-semibold );
174+ }
175+
176+ .file-tree-search-no-results {
177+ padding : 1rem ;
178+ text-align : center ;
179+ color : var (--color-text-light-2 );
180+ font-size : 14px ;
181+ }
38182 </style >
0 commit comments