fix(tui): scope file autocomplete to session (#33458)
This commit is contained in:
parent
975b1132f1
commit
ef2357915e
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -65,7 +65,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
run: bun turbo test --output-logs=errors-only --log-order=grouped --log-prefix=none
|
run: GITHUB_ACTIONS=false bun turbo test
|
||||||
env:
|
env:
|
||||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
|
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
|
||||||
|
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import { SDKProvider, useSDK } from "./context/sdk"
|
|||||||
import { StartupLoading } from "./component/startup-loading"
|
import { StartupLoading } from "./component/startup-loading"
|
||||||
import { SyncProvider, useSync } from "./context/sync"
|
import { SyncProvider, useSync } from "./context/sync"
|
||||||
import { DataProvider } from "./context/data"
|
import { DataProvider } from "./context/data"
|
||||||
|
import { LocationProvider } from "./context/location"
|
||||||
import { LocalProvider, useLocal } from "./context/local"
|
import { LocalProvider, useLocal } from "./context/local"
|
||||||
import { DialogModel } from "./component/dialog-model"
|
import { DialogModel } from "./component/dialog-model"
|
||||||
import { useConnected } from "./component/use-connected"
|
import { useConnected } from "./component/use-connected"
|
||||||
@ -303,10 +304,12 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
|||||||
<PromptHistoryProvider>
|
<PromptHistoryProvider>
|
||||||
<PromptRefProvider>
|
<PromptRefProvider>
|
||||||
<EditorContextProvider>
|
<EditorContextProvider>
|
||||||
<App
|
<LocationProvider>
|
||||||
onSnapshot={input.onSnapshot}
|
<App
|
||||||
pluginHost={input.pluginHost}
|
onSnapshot={input.onSnapshot}
|
||||||
/>
|
pluginHost={input.pluginHost}
|
||||||
|
/>
|
||||||
|
</LocationProvider>
|
||||||
</EditorContextProvider>
|
</EditorContextProvider>
|
||||||
</PromptRefProvider>
|
</PromptRefProvider>
|
||||||
</PromptHistoryProvider>
|
</PromptHistoryProvider>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { useData } from "../../context/data"
|
|||||||
import { getScrollAcceleration } from "../../util/scroll"
|
import { getScrollAcceleration } from "../../util/scroll"
|
||||||
import { useTuiPaths } from "../../context/runtime"
|
import { useTuiPaths } from "../../context/runtime"
|
||||||
import { useTuiConfig } from "../../config"
|
import { useTuiConfig } from "../../config"
|
||||||
|
import { useLocation } from "../../context/location"
|
||||||
import { useTheme, selectedForeground } from "../../context/theme"
|
import { useTheme, selectedForeground } from "../../context/theme"
|
||||||
import { SplitBorder } from "../../ui/border"
|
import { SplitBorder } from "../../ui/border"
|
||||||
import { useTerminalDimensions } from "@opentui/solid"
|
import { useTerminalDimensions } from "@opentui/solid"
|
||||||
@ -21,6 +22,7 @@ import type { PromptInfo } from "../../prompt/history"
|
|||||||
import { useFrecency } from "../../prompt/frecency"
|
import { useFrecency } from "../../prompt/frecency"
|
||||||
import { useBindings, useCommandSlashes, useOpencodeModeStack } from "../../keymap"
|
import { useBindings, useCommandSlashes, useOpencodeModeStack } from "../../keymap"
|
||||||
import { displayCharAt, mentionTriggerIndex } from "../../prompt/display"
|
import { displayCharAt, mentionTriggerIndex } from "../../prompt/display"
|
||||||
|
import type { FileSystemEntry } from "@opencode-ai/sdk/v2"
|
||||||
|
|
||||||
function removeLineRange(input: string) {
|
function removeLineRange(input: string) {
|
||||||
const hashIndex = input.lastIndexOf("#")
|
const hashIndex = input.lastIndexOf("#")
|
||||||
@ -94,6 +96,7 @@ export function Autocomplete(props: {
|
|||||||
const frecency = useFrecency()
|
const frecency = useFrecency()
|
||||||
const tuiConfig = useTuiConfig()
|
const tuiConfig = useTuiConfig()
|
||||||
const paths = useTuiPaths()
|
const paths = useTuiPaths()
|
||||||
|
const location = useLocation()
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
index: 0,
|
index: 0,
|
||||||
selected: 0,
|
selected: 0,
|
||||||
@ -236,16 +239,18 @@ export function Autocomplete(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFilePart(item: string, lineRange?: { startLine: number; endLine?: number }) {
|
function createFilePart(
|
||||||
const baseDir = (sync.path.directory || paths.cwd).replace(/\/+$/, "")
|
item: FileSystemEntry,
|
||||||
const fullPath = path.isAbsolute(item) ? item : path.join(baseDir, item)
|
filePath: string,
|
||||||
const urlObj = pathToFileURL(fullPath)
|
lineRange?: { startLine: number; endLine?: number },
|
||||||
|
) {
|
||||||
|
const urlObj = pathToFileURL(filePath)
|
||||||
const filename =
|
const filename =
|
||||||
lineRange && !item.endsWith("/")
|
lineRange && item.type !== "directory"
|
||||||
? `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
|
? `${item.path}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
|
||||||
: item
|
: item.path
|
||||||
|
|
||||||
if (lineRange && !item.endsWith("/")) {
|
if (lineRange && item.type !== "directory") {
|
||||||
urlObj.searchParams.set("start", String(lineRange.startLine))
|
urlObj.searchParams.set("start", String(lineRange.startLine))
|
||||||
if (lineRange.endLine !== undefined) {
|
if (lineRange.endLine !== undefined) {
|
||||||
urlObj.searchParams.set("end", String(lineRange.endLine))
|
urlObj.searchParams.set("end", String(lineRange.endLine))
|
||||||
@ -254,10 +259,9 @@ export function Autocomplete(props: {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
filename,
|
filename,
|
||||||
url: urlObj.href,
|
|
||||||
part: {
|
part: {
|
||||||
type: "file" as const,
|
type: "file" as const,
|
||||||
mime: "text/plain",
|
mime: item.mime,
|
||||||
filename,
|
filename,
|
||||||
url: urlObj.href,
|
url: urlObj.href,
|
||||||
source: {
|
source: {
|
||||||
@ -267,7 +271,7 @@ export function Autocomplete(props: {
|
|||||||
end: 0,
|
end: 0,
|
||||||
value: "",
|
value: "",
|
||||||
},
|
},
|
||||||
path: item,
|
path: item.path,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -284,7 +288,7 @@ export function Autocomplete(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function normalizeMentionPath(filePath: string) {
|
function normalizeMentionPath(filePath: string) {
|
||||||
const baseDir = sync.path.directory || paths.cwd
|
const baseDir = location()?.directory || sync.path.directory || paths.cwd
|
||||||
const absolute = path.resolve(filePath)
|
const absolute = path.resolve(filePath)
|
||||||
const relative = path.relative(baseDir, absolute)
|
const relative = path.relative(baseDir, absolute)
|
||||||
|
|
||||||
@ -301,7 +305,11 @@ export function Autocomplete(props: {
|
|||||||
startLine: input.lineStart,
|
startLine: input.lineStart,
|
||||||
endLine: input.lineEnd > input.lineStart ? input.lineEnd : undefined,
|
endLine: input.lineEnd > input.lineStart ? input.lineEnd : undefined,
|
||||||
}
|
}
|
||||||
const { filename, part } = createFilePart(item, lineRange)
|
const { filename, part } = createFilePart(
|
||||||
|
{ path: item, type: "file", mime: "text/plain" },
|
||||||
|
input.filePath,
|
||||||
|
lineRange,
|
||||||
|
)
|
||||||
const index = store.visible === "@" ? store.index : props.input().cursorOffset
|
const index = store.visible === "@" ? store.index : props.input().cursorOffset
|
||||||
|
|
||||||
setStore("visible", false)
|
setStore("visible", false)
|
||||||
@ -310,17 +318,20 @@ export function Autocomplete(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [files] = createResource(
|
const [files] = createResource(
|
||||||
() => search(),
|
() => ({ query: search(), location: location() }),
|
||||||
async (query) => {
|
async (input) => {
|
||||||
if (!store.visible || store.visible === "/") return []
|
if (!store.visible || store.visible === "/") return []
|
||||||
if (referenceMatch()) return []
|
if (referenceMatch()) return []
|
||||||
const { lineRange, baseQuery } = extractLineRange(query ?? "")
|
const { lineRange, baseQuery } = extractLineRange(input.query ?? "")
|
||||||
|
|
||||||
// Get files from SDK
|
// Get files from SDK
|
||||||
const result = await sdk.client.v2.fs.find({
|
const result = await sdk.client.v2.fs.find({
|
||||||
query: baseQuery,
|
query: baseQuery,
|
||||||
limit: "20",
|
limit: "20",
|
||||||
location: { workspace: project.workspace.current() },
|
location: {
|
||||||
|
directory: input.location?.directory,
|
||||||
|
workspace: input.location?.workspaceID ?? project.workspace.current(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const options: AutocompleteOption[] = []
|
const options: AutocompleteOption[] = []
|
||||||
@ -331,7 +342,11 @@ export function Autocomplete(props: {
|
|||||||
const width = props.anchor().width - 4
|
const width = props.anchor().width - 4
|
||||||
options.push(
|
options.push(
|
||||||
...result.data.data.map((item): AutocompleteOption => {
|
...result.data.data.map((item): AutocompleteOption => {
|
||||||
const { filename, url, part } = createFilePart(item.path, lineRange)
|
const { filename, part } = createFilePart(
|
||||||
|
item,
|
||||||
|
path.join(result.data.location.directory, item.path),
|
||||||
|
lineRange,
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
display: Locale.truncateMiddle(filename, width),
|
display: Locale.truncateMiddle(filename, width),
|
||||||
value: filename,
|
value: filename,
|
||||||
|
|||||||
14
packages/tui/src/context/location.tsx
Normal file
14
packages/tui/src/context/location.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { LocationRef } from "@opencode-ai/sdk/v2"
|
||||||
|
import { createContext, useContext, type Accessor, type ParentProps } from "solid-js"
|
||||||
|
|
||||||
|
const context = createContext<Accessor<LocationRef | undefined>>()
|
||||||
|
|
||||||
|
export function LocationProvider(props: ParentProps<{ location?: LocationRef }>) {
|
||||||
|
return <context.Provider value={() => props.location}>{props.children}</context.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLocation() {
|
||||||
|
const value = useContext(context)
|
||||||
|
if (!value) throw new Error("Location context must be used within a LocationProvider")
|
||||||
|
return value
|
||||||
|
}
|
||||||
@ -1,31 +1,15 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { createContext, useContext, type ParentProps } from "solid-js"
|
|
||||||
import { abbreviateHome } from "../runtime"
|
import { abbreviateHome } from "../runtime"
|
||||||
|
import { useLocation } from "./location"
|
||||||
import { useTuiPaths } from "./runtime"
|
import { useTuiPaths } from "./runtime"
|
||||||
|
|
||||||
const context = createContext<{
|
|
||||||
path: () => string
|
|
||||||
format: (input?: string) => string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
export function PathFormatterProvider(props: ParentProps<{ path: string | undefined }>) {
|
|
||||||
const paths = useTuiPaths()
|
|
||||||
return (
|
|
||||||
<context.Provider
|
|
||||||
value={{
|
|
||||||
path: () => props.path || paths.cwd,
|
|
||||||
format: (input) => formatPath(input, props.path || paths.cwd, paths.home),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</context.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePathFormatter() {
|
export function usePathFormatter() {
|
||||||
const value = useContext(context)
|
const paths = useTuiPaths()
|
||||||
if (!value) throw new Error("PathFormatter context must be used within a PathFormatterProvider")
|
const location = useLocation()
|
||||||
return value
|
return {
|
||||||
|
path: () => location()?.directory || paths.cwd,
|
||||||
|
format: (input?: string) => formatPath(input, location()?.directory || paths.cwd, paths.home),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPath(input: string | undefined, base: string, home: string) {
|
function formatPath(input: string | undefined, base: string, home: string) {
|
||||||
|
|||||||
@ -80,7 +80,8 @@ import { usePluginRuntime } from "../../plugin/runtime"
|
|||||||
import { DialogRetryAction } from "../../component/dialog-retry-action"
|
import { DialogRetryAction } from "../../component/dialog-retry-action"
|
||||||
import { getRevertDiffFiles } from "../../util/revert-diff"
|
import { getRevertDiffFiles } from "../../util/revert-diff"
|
||||||
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useOpencodeKeymap } from "../../keymap"
|
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useOpencodeKeymap } from "../../keymap"
|
||||||
import { PathFormatterProvider, usePathFormatter } from "../../context/path-format"
|
import { usePathFormatter } from "../../context/path-format"
|
||||||
|
import { LocationProvider } from "../../context/location"
|
||||||
|
|
||||||
addDefaultParsers(parsers.parsers)
|
addDefaultParsers(parsers.parsers)
|
||||||
|
|
||||||
@ -193,6 +194,10 @@ export function Session() {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const promptRef = usePromptRef()
|
const promptRef = usePromptRef()
|
||||||
const session = createMemo(() => sync.session.get(route.sessionID))
|
const session = createMemo(() => sync.session.get(route.sessionID))
|
||||||
|
const location = createMemo(() => {
|
||||||
|
const current = session()
|
||||||
|
return current ? { directory: current.directory, workspaceID: current.workspaceID } : undefined
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const title = Locale.truncate(session()?.title ?? "", 50)
|
const title = Locale.truncate(session()?.title ?? "", 50)
|
||||||
@ -1138,7 +1143,7 @@ export function Session() {
|
|||||||
createEffect(on(() => route.sessionID, toBottom))
|
createEffect(on(() => route.sessionID, toBottom))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PathFormatterProvider path={session()?.directory}>
|
<LocationProvider location={location()}>
|
||||||
<context.Provider
|
<context.Provider
|
||||||
value={{
|
value={{
|
||||||
get width() {
|
get width() {
|
||||||
@ -1338,7 +1343,7 @@ export function Session() {
|
|||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
</context.Provider>
|
</context.Provider>
|
||||||
</PathFormatterProvider>
|
</LocationProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user