Skip to content
Merged
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
5 changes: 3 additions & 2 deletions packages/client/src/renderer/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2815,8 +2815,9 @@
"repos": "Search repositories for \"{query}\"",
"users": "Search users for \"{query}\""
},
"description": "Search for GitHub accounts, organizations, and repositories.",
"placeholder": "Search GitHub or paste a URL",
"description": "Search open tabs, GitHub accounts, organizations, and repositories.",
"openTab": "Already open",
"placeholder": "Search tabs or GitHub, or paste a URL",
"resolveError": "Could not resolve this target. Try a search instead.",
"title": "Search workspace"
},
Expand Down
5 changes: 3 additions & 2 deletions packages/client/src/renderer/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -2815,8 +2815,9 @@
"repos": "搜索仓库 \"{query}\"",
"users": "搜索用户 \"{query}\""
},
"description": "搜索 GitHub 账号、组织和仓库。",
"placeholder": "搜索 GitHub 或粘贴 URL",
"description": "搜索已打开的标签页、GitHub 账号、组织和仓库。",
"openTab": "已打开的 Tab",
"placeholder": "搜索标签页或 GitHub,也可以粘贴 URL",
"resolveError": "无法解析这个目标。可以改用搜索。",
"title": "搜索工作区"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { WorkspaceTab } from '@/pages/workspace/types'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFuse } from '@vueuse/integrations/useFuse'
Expand All @@ -11,6 +12,7 @@ import {
UserRound,
} from 'lucide-vue-next'
import {
Badge,
CommandGroup,
CommandInput,
CommandItem,
Expand All @@ -20,12 +22,14 @@ import {
useCommand,
} from '@oh-my-github/ui'
import { useViewerRepositoriesQuery } from '@/composables/github/use-accounts'
import { getWorkspaceTabView } from '@/pages/workspace/tab-presentation'
import { createRepositoryWorkspaceUrl } from '@/pages/workspace/workspace-url'

const props = defineProps<{
isResolving: boolean
open: boolean
resolveError: boolean
tabs: WorkspaceTab[]
}>()

const emit = defineEmits<{
Expand All @@ -39,6 +43,36 @@ const { filterState } = useCommand()
const latestQuery = ref('')
const query = computed(() => filterState.search.trim())
const hasQuery = computed(() => query.value.length > 0)
const openTabUrls = computed(() => new Set(props.tabs.map((tab) => tab.url)))
const openTabs = computed(() => props.tabs.map((tab) => {
const view = getWorkspaceTabView(tab)
const title = view.titleKey ? t(view.titleKey, view.titleParams ?? {}) : view.title

return {
icon: view.icon,
searchText: [title, tab.url, tab.owner, tab.repo, tab.type].filter(Boolean).join(' '),
tab,
title,
}
}))

const MAX_OPEN_TAB_MATCHES = 6
const { results: openTabFuseResults } = useFuse(query, openTabs, {
matchAllWhenSearchEmpty: false,
fuseOptions: {
keys: [
{ name: 'title', weight: 2 },
{ name: 'searchText', weight: 1 },
],
threshold: 0.4,
ignoreLocation: true,
},
})
const openTabMatches = computed(() =>
hasQuery.value
? openTabFuseResults.value.slice(0, MAX_OPEN_TAB_MATCHES).map((result) => result.item)
: [],
)

// Local cache of every repo the viewer can reach, for instant fuzzy navigation. Shares
// the session-warm Colada query prefetched by the workspace shell, so opening the palette
Expand All @@ -59,7 +93,12 @@ const { results: fuseResults } = useFuse(query, repositories, {
},
})
const repoMatches = computed(() =>
hasQuery.value ? fuseResults.value.slice(0, MAX_REPO_MATCHES).map((result) => result.item) : [],
hasQuery.value
? fuseResults.value
.map((result) => result.item)
.filter((repository) => !openTabUrls.value.has(createRepositoryWorkspaceUrl(repository.owner, repository.name)))
.slice(0, MAX_REPO_MATCHES)
: [],
)

watch(query, (value) => {
Expand Down Expand Up @@ -107,6 +146,33 @@ function openRepository(repository: GitHubRepository): void {
v-if="hasQuery"
:key="query"
>
<CommandGroup
v-if="openTabMatches.length"
force-render
>
<CommandItem
v-for="item in openTabMatches"
:key="item.tab.url"
force-render
:value="`tab:${item.searchText}`"
@select="emit('navigate', item.tab.url)"
>
<component
:is="item.icon"
class="size-3.5"
/>
<span class="min-w-0 flex-1 truncate">{{ item.title }}</span>
<Badge
class="select-none shrink-0"
variant="secondary"
>
{{ t('workspace.search.openTab') }}
</Badge>
</CommandItem>
</CommandGroup>

<CommandSeparator v-if="openTabMatches.length" />

<!-- Repositories matched against the local cache: pressing Enter jumps straight to the
top match, which is what most Ctrl-K users want. Rendered first so it stays the
default; when nothing matches, the repository-search action below is the fallback. -->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<script setup lang="ts">
import type { WorkspaceTab } from '@/pages/workspace/types'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { CommandDialog } from '@oh-my-github/ui'
import WorkspaceSearchDialogContent from './workspace-search-dialog-content.vue'

const props = defineProps<{
open: boolean
tabs: WorkspaceTab[]
}>()

const emit = defineEmits<{
Expand Down Expand Up @@ -96,6 +98,7 @@ function createNotFoundUrl(input: string): string {
:is-resolving="isResolving"
:open="props.open"
:resolve-error="resolveError"
:tabs="props.tabs"
@goto="gotoQuery"
@search="searchQuery"
@navigate="navigateToUrl"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const rightPanelLabel = computed(() =>
t(isRightPanelOpen.value ? 'workspace.rightPanel.close' : 'workspace.rightPanel.open'),
)
const hasTabOverflow = computed(() => scrollMetrics.value.scrollWidth > scrollMetrics.value.clientWidth + 1)
const canScrollTabsLeft = computed(() => hasTabOverflow.value && scrollMetrics.value.scrollLeft > 1)
const canScrollTabsRight = computed(() => {
const { clientWidth, scrollLeft, scrollWidth } = scrollMetrics.value
return hasTabOverflow.value && scrollLeft + clientWidth < scrollWidth - 1
})
const scrollbarThumbStyle = computed(() => {
const { clientWidth, scrollLeft, scrollWidth } = scrollMetrics.value

Expand Down Expand Up @@ -350,7 +355,7 @@ watch(
class="min-h-0 flex-1 gap-0"
@update:model-value="setActiveTab"
>
<div class="workspace-tabs-bar flex h-9 shrink-0 items-center gap-1 border-b border-border bg-background px-2">
<div class="workspace-tabs-bar flex h-10 shrink-0 items-center gap-1 border-b border-border bg-background px-2">
<div
v-if="shouldReserveTrafficLights"
aria-hidden="true"
Expand Down Expand Up @@ -469,6 +474,8 @@ watch(

<div
class="workspace-tabs-scroll-frame min-w-0 flex-1"
:data-can-scroll-left="canScrollTabsLeft ? 'true' : undefined"
:data-can-scroll-right="canScrollTabsRight ? 'true' : undefined"
:data-overflowing="hasTabOverflow ? 'true' : undefined"
>
<div
Expand Down Expand Up @@ -502,7 +509,7 @@ watch(
variant="ghost"
@click.stop="emit('close', tab.url)"
>
<X class="size-3" />
<X class="size-3.5" />
</Button>
</div>
</ContextMenuTrigger>
Expand Down Expand Up @@ -680,6 +687,38 @@ watch(
position: relative;
}

.workspace-tabs-scroll-frame::before,
.workspace-tabs-scroll-frame::after {
position: absolute;
z-index: 1;
top: 0;
bottom: 0;
width: 2rem;
content: '';
pointer-events: none;
opacity: 0;
transition: opacity 0.16s ease;
}

.workspace-tabs-scroll-frame::before {
left: 0;
background: linear-gradient(to right, var(--background), transparent);
backdrop-filter: blur(2px);
mask-image: linear-gradient(to right, black, transparent);
}

.workspace-tabs-scroll-frame::after {
right: 0;
background: linear-gradient(to left, var(--background), transparent);
backdrop-filter: blur(2px);
mask-image: linear-gradient(to left, black, transparent);
}

.workspace-tabs-scroll-frame[data-can-scroll-left="true"]::before,
.workspace-tabs-scroll-frame[data-can-scroll-right="true"]::after {
opacity: 1;
}

.workspace-tabs-scroll {
scrollbar-width: none;
}
Expand All @@ -690,6 +729,7 @@ watch(

.workspace-tabs-scrollbar {
position: absolute;
z-index: 2;
right: 0;
bottom: 0.125rem;
left: 0;
Expand Down Expand Up @@ -718,7 +758,7 @@ watch(
display: flex;
min-width: 0;
align-items: center;
height: 1.75rem;
height: 2rem;
border-radius: var(--radius-md);
color: var(--muted-foreground);
}
Expand All @@ -734,12 +774,12 @@ watch(
}

[data-workspace-tabs] :deep([data-slot="tabs-trigger"]) {
height: 1.75rem;
padding-inline: 0.5rem 0.25rem;
height: 2rem;
padding-inline: 0.75rem 0.5rem;
border-radius: var(--radius-md);
font-size: var(--text-body);
line-height: var(--text-body--line-height);
letter-spacing: var(--text-body--letter-spacing);
font-size: var(--text-label);
line-height: var(--text-label--line-height);
letter-spacing: var(--text-label--letter-spacing);
color: inherit;
}

Expand All @@ -754,17 +794,18 @@ watch(
}

[data-workspace-tabs] :deep([data-slot="tabs-trigger"] svg) {
width: 0.875rem;
height: 0.875rem;
width: 1rem;
height: 1rem;
}

[data-workspace-tabs] :deep(.tabs-underline) {
display: none;
}

.workspace-tab-close {
width: 1.375rem;
height: 1.375rem;
width: 1.5rem;
height: 1.5rem;
margin-right: 0.25rem;
color: inherit;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ async function writeClipboardText(value: string): Promise<void> {

<WorkspaceSearchDialog
v-model:open="isSearchDialogOpen"
:tabs="tabs"
@navigate="selectTab"
/>
</SidebarProvider>
Expand Down
Loading