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