Порівняти коміти

...
2 Коміти
Автор SHA1 Повідомлення Дата
arookieofc 5df2c763a4 Merge branch 'main' into feat/cache-dir-1722 2026-11-11 23:52:57 +08:00
arookieofc 6b32100980 feat(settings): custom local data directory with one-click migration (#1722) 2026-11-11 20:24:26 +08:00
8 змінених файлів з 546 додано та 29 видалено

No files matched your search

+320 -26
Переглянути файл
@@ -9,6 +9,14 @@ use std::path::{Path, PathBuf};
/// configuration and roaming data keep following the OS.
pub const LOCAL_DATA_ENV: &str = "OPENIDEA_LOCAL_DATA_DIR";
/// A custom local-data root chosen in Settings › System Settings › Caches,
/// stored in the configuration directory. The environment variable still wins.
pub const OVERRIDE_FILE: &str = "local-data-dir.txt";
/// A pending move of the local data to a newly chosen directory, carried out
/// at the next start (before any project opens), like File › Invalidate Caches.
const MIGRATION_FILE: &str = "local-data-migration.json";
/// `$OPENIDEA_LOCAL_DATA_DIR/<identifier>` when set, else the OS local data directory
/// (Tauri's `app_local_data_dir`). The identifier keeps release and per-worktree
/// debug builds apart under one root. A set but unusable path is an error, never a
@@ -17,6 +25,203 @@ pub fn local_data_dir(identifier: &str) -> Result<PathBuf, String> {
resolve_local(std::env::var_os(LOCAL_DATA_ENV), dirs::data_local_dir(), identifier)
}
/// The directory to actually start with: the environment override, else a valid
/// custom directory from Settings, else the OS default. A stored but unusable
/// custom directory is an error, never a silent fallback.
pub fn startup_local_data_dir(identifier: &str) -> Result<PathBuf, String> {
startup_resolve(std::env::var_os(LOCAL_DATA_ENV), stored_override(identifier), dirs::data_local_dir(), identifier)
}
fn startup_resolve(
var: Option<OsString>,
stored: Option<PathBuf>,
os: Option<PathBuf>,
identifier: &str,
) -> Result<PathBuf, String> {
if var.as_ref().is_some_and(|v| !v.is_empty()) {
return resolve_local(var, os, identifier);
}
let Some(root) = stored else { return resolve_local(None, os, identifier) };
if !root.is_absolute() {
return Err(format!("custom local data directory must be an absolute path: {}", root.display()));
}
resolve_root(root, identifier, "custom local data directory")
}
/// The file holding the custom local-data root, if the platform exposes its
/// configuration directory. Both startup and Settings use this one path.
pub fn override_path(identifier: &str) -> Option<PathBuf> {
dirs::config_dir().map(|dir| dir.join(identifier).join(OVERRIDE_FILE))
}
fn migration_path(identifier: &str) -> Option<PathBuf> {
dirs::config_dir().map(|dir| dir.join(identifier).join(MIGRATION_FILE))
}
/// The custom root stored by Settings, if any (validated when it is used).
pub fn stored_override(identifier: &str) -> Option<PathBuf> {
stored_override_in(&override_path(identifier)?)
}
fn stored_override_in(path: &Path) -> Option<PathBuf> {
let text = fs::read_to_string(path).ok()?;
let trimmed = text.trim();
(!trimmed.is_empty()).then(|| PathBuf::from(trimmed))
}
/// The OS default local data directory (what clearing the custom directory
/// returns to), ignoring any stored override. The environment still applies.
pub fn default_local_data_dir(identifier: &str) -> Result<PathBuf, String> {
local_data_dir(identifier)
}
/// Stores (`Some`) or clears (`None`) the custom local-data root, returning the
/// directory that takes effect after a restart. A pending custom migration to
/// anywhere else is dropped: only the caller schedules the matching one.
pub fn set_override(identifier: &str, root: Option<&Path>) -> Result<PathBuf, String> {
let Some(path) = override_path(identifier) else {
return Err("cannot find the configuration directory".into());
};
set_override_in(&path, &migration_path(identifier), root, identifier)
}
fn set_override_in(
path: &Path,
migration: &Option<PathBuf>,
root: Option<&Path>,
identifier: &str,
) -> Result<PathBuf, String> {
if let Some(migration) = migration {
let _ = fs::remove_file(migration);
}
let Some(root) = root else {
if let Err(error) = fs::remove_file(path)
&& error.kind() != io::ErrorKind::NotFound
{
return Err(format!("cannot clear the custom local data directory: {error}"));
}
return local_data_dir(identifier);
};
if !root.is_absolute() {
return Err(format!("custom local data directory must be an absolute path: {}", root.display()));
}
let dir = resolve_root(root.to_path_buf(), identifier, "custom local data directory")?;
if let Some(parent) = path.parent()
&& let Err(error) = fs::create_dir_all(parent)
{
return Err(format!("cannot store the custom local data directory: {error}"));
}
if let Err(error) = fs::write(path, format!("{}\n", root.display())) {
return Err(format!("cannot store the custom local data directory: {error}"));
}
Ok(dir)
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct CustomMigration {
from: PathBuf,
to: PathBuf,
}
/// Schedules a move of the local data from `from` to `to` at the next start.
/// Returns false when there is nothing to move. The caller restarts the IDE.
pub fn schedule_custom_migration(identifier: &str, from: &Path, to: &Path) -> Result<bool, String> {
let Some(path) = migration_path(identifier) else {
return Err("cannot find the configuration directory".into());
};
schedule_custom_migration_in(&path, from, to)
}
fn schedule_custom_migration_in(path: &Path, from: &Path, to: &Path) -> Result<bool, String> {
if from == to {
return Ok(false);
}
if let Some(parent) = path.parent()
&& let Err(error) = fs::create_dir_all(parent)
{
return Err(format!("cannot schedule the data move: {error}"));
}
let json = serde_json::to_vec(&CustomMigration { from: from.into(), to: to.into() })
.map_err(|error| format!("cannot schedule the data move: {error}"))?;
if let Err(error) = fs::write(path, json) {
return Err(format!("cannot schedule the data move: {error}"));
}
Ok(true)
}
/// Carries out a scheduled custom migration aimed at `local` (call at start-up,
/// before projects open). Entries that appear in the meantime are never
/// overwritten; a partially moved tree is retried at the next start.
pub fn apply_custom_migration(identifier: &str, local: &Path) {
let Some(path) = migration_path(identifier) else { return };
apply_custom_migration_in(&path, local);
}
fn apply_custom_migration_in(path: &Path, local: &Path) {
let Ok(bytes) = fs::read(path) else { return };
let Ok(job) = serde_json::from_slice::<CustomMigration>(&bytes) else {
let _ = fs::remove_file(path);
return;
};
if job.to != local || job.from == local {
let _ = fs::remove_file(path);
return;
}
move_entries(&job.from, local, &custom_entries(&job.from));
let leftover = custom_entries(&job.from).into_iter().any(|entry| job.from.join(entry).exists());
if !leftover {
let _ = fs::remove_file(path);
}
}
/// Entries of a local-data tree that move to a newly chosen directory.
/// Non-relocatable tool installs (virtual environments with absolute interpreter
/// paths) stay behind and are reinstalled on demand, as in `migrate_entries`.
fn custom_entries(root: &Path) -> Vec<PathBuf> {
let mut entries = local_entries(root);
for extra in ["logs", "agents/tools"] {
let entry = PathBuf::from(extra);
if !entries.contains(&entry) {
entries.push(entry);
}
}
entries
}
/// Moves the local-data tree from `from` to `to`, entry by entry. Same-volume
/// entries rename at once; cross-volume copies run in the background. Targets
/// created in the meantime are never merged into or overwritten.
fn move_entries(from: &Path, to: &Path, entries: &[PathBuf]) {
if from == to {
return;
}
let mut copies = Vec::new();
for entry in entries {
let source = from.join(entry);
let target = to.join(entry);
match try_rename(&source, &target) {
Ok(true) => copies.push((source, target)),
Ok(false) => {}
Err(error) => {
tracing::warn!(?source, ?target, %error, "data move failed; keeping old data, using new location")
}
}
}
if !copies.is_empty() {
let result = std::thread::Builder::new().name("openidea-data-migration".into()).spawn(move || {
for (source, target) in copies {
tracing::info!(?source, ?target, "copying data to the new location in background");
if let Err(error) = copy_then_remove(&source, &target) {
tracing::warn!(?source, ?target, %error, "data copy failed; keeping old data");
}
}
});
if let Err(error) = result {
tracing::warn!(%error, "cannot start data migration worker; keeping old data");
}
}
}
fn resolve_local(var: Option<OsString>, os: Option<PathBuf>, identifier: &str) -> Result<PathBuf, String> {
let Some(root) = var.filter(|v| !v.is_empty()).map(PathBuf::from) else {
return os.map(|dir| dir.join(identifier)).ok_or_else(|| "cannot find the local data directory".into());
@@ -25,10 +230,14 @@ fn resolve_local(var: Option<OsString>, os: Option<PathBuf>, identifier: &str) -
if !root.is_absolute() {
return Err(format!("{LOCAL_DATA_ENV} must be an absolute path: {}", root.display()));
}
resolve_root(root, identifier, LOCAL_DATA_ENV)
}
fn resolve_root(root: PathBuf, identifier: &str, what: &str) -> Result<PathBuf, String> {
let dir = root.join(identifier);
fs::create_dir_all(&dir)
.and_then(|()| tempfile::NamedTempFile::new_in(&dir).map(drop))
.map_err(|error| format!("{LOCAL_DATA_ENV}: cannot write to {}: {error}", dir.display()))?;
.map_err(|error| format!("{what}: cannot write to {}: {error}", dir.display()))?;
Ok(dir)
}
@@ -77,6 +286,11 @@ pub fn migrate(data: &Path, local: &Path) {
}
fn migrate_entries(data: &Path, local: &Path) {
move_entries(data, local, &local_entries(data));
}
/// The rebuildable entries of the legacy roaming tree that move to Local.
fn local_entries(data: &Path) -> Vec<PathBuf> {
let mut entries: Vec<PathBuf> = [
"language-server-runtimes",
"index",
@@ -118,31 +332,7 @@ fn migrate_entries(data: &Path, local: &Path) {
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => tracing::warn!(%error, "cannot enumerate legacy tools; keeping old data, using Local"),
}
let mut copies = Vec::new();
for entry in entries {
let source = data.join(&entry);
let target = data_path(data, local, &entry);
match try_rename(&source, &target) {
Ok(true) => copies.push((source, target)),
Ok(false) => {}
Err(error) => {
tracing::warn!(?source, ?target, %error, "data migration failed; keeping old data, using Local")
}
}
}
if !copies.is_empty() {
let result = std::thread::Builder::new().name("openidea-data-migration".into()).spawn(move || {
for (source, target) in copies {
tracing::info!(?source, ?target, "copying legacy data in background");
if let Err(error) = copy_then_remove(&source, &target) {
tracing::warn!(?source, ?target, %error, "data copy failed; keeping old data, using Local");
}
}
});
if let Err(error) = result {
tracing::warn!(%error, "cannot start data migration worker; keeping old data, using Local");
}
}
entries
}
/// Returns true when a cross-volume copy is needed.
@@ -597,4 +787,108 @@ mod tests {
.any(|e| e.file_name().to_string_lossy().starts_with(".migration-"))
);
}
#[test]
fn custom_override_round_trips_and_validates() {
let tmp = tempfile::tempdir().unwrap();
let file = tmp.path().join("local-data-dir.txt");
let migration = tmp.path().join("local-data-migration.json");
let id = "ai.test.openidea";
assert_eq!(stored_override_in(&file), None);
let root = tmp.path().join("custom-root");
let dir = set_override_in(&file, &Some(migration.clone()), Some(&root), id).unwrap();
assert_eq!(dir, root.join(id));
assert!(dir.is_dir());
assert_eq!(stored_override_in(&file), Some(root.clone()));
assert_eq!(startup_resolve(None, stored_override_in(&file), Some(tmp.path().join("os")), id).unwrap(), dir);
// A relative root is rejected before anything is written.
let before = fs::read_to_string(&file).unwrap();
let error = set_override_in(&file, &Some(migration.clone()), Some(Path::new("relative")), id).unwrap_err();
assert!(error.contains("absolute"), "{error}");
assert_eq!(fs::read_to_string(&file).unwrap(), before);
// An unwritable root (an existing file) is rejected too.
let blocker = tmp.path().join("blocker");
fs::write(&blocker, "").unwrap();
let error = set_override_in(&file, &Some(migration.clone()), Some(&blocker), id).unwrap_err();
assert!(error.contains("cannot write"), "{error}");
// Clearing removes the file and returns the default.
let dir = set_override_in(&file, &Some(migration), None, id).unwrap();
assert!(!file.exists());
assert_eq!(dir, local_data_dir(id).unwrap());
assert_eq!(stored_override_in(&file), None);
}
#[test]
fn startup_prefers_the_environment_then_the_override() {
let tmp = tempfile::tempdir().unwrap();
let os = tmp.path().join("os");
let custom = tmp.path().join("custom");
let env = tmp.path().join("env");
let id = "ai.test.openidea";
let error = startup_resolve(None, Some(PathBuf::from("relative")), Some(os.clone()), id).unwrap_err();
assert!(error.contains("absolute"), "{error}");
// The environment wins over a stored override.
let dir = startup_resolve(Some(env.clone().into()), Some(custom.clone()), Some(os.clone()), id).unwrap();
assert_eq!(dir, env.join(id));
// Without the environment the override wins over the OS default.
let dir = startup_resolve(None, Some(custom.clone()), Some(os.clone()), id).unwrap();
assert_eq!(dir, custom.join(id));
// And without either the OS default applies.
let dir = startup_resolve(None, None, Some(os.clone()), id).unwrap();
assert_eq!(dir, os.join(id));
}
#[test]
fn custom_migration_moves_the_tree_and_clears_itself() {
let tmp = tempfile::tempdir().unwrap();
let marker = tmp.path().join("local-data-migration.json");
let from = tmp.path().join("old-local");
let to = tmp.path().join("new-local");
fs::create_dir_all(from.join("index/p1")).unwrap();
fs::write(from.join("index/p1/symbols"), "index").unwrap();
fs::create_dir_all(from.join("cache/p1")).unwrap();
fs::write(from.join("cache/p1/build"), "cache").unwrap();
fs::create_dir_all(from.join("tools/demo/bin")).unwrap();
fs::write(from.join("tools/demo/bin/demo.cmd"), "payload").unwrap();
fs::write(from.join("history"), "roaming stays").unwrap();
// Nothing to move: no marker is written.
assert!(!schedule_custom_migration_in(&marker, &from, &from).unwrap());
assert!(!marker.exists());
assert!(schedule_custom_migration_in(&marker, &from, &to).unwrap());
apply_custom_migration_in(&marker, &to);
assert!(!marker.exists());
assert_eq!(fs::read_to_string(to.join("index/p1/symbols")).unwrap(), "index");
assert_eq!(fs::read_to_string(to.join("cache/p1/build")).unwrap(), "cache");
assert!(to.join("tools/demo/bin/demo.cmd").exists());
assert!(!from.join("index").exists());
// Roaming content is not local data and stays behind.
assert_eq!(fs::read_to_string(from.join("history")).unwrap(), "roaming stays");
// A migration aimed elsewhere is dropped without touching anything.
fs::create_dir_all(from.join("lsp")).unwrap();
assert!(schedule_custom_migration_in(&marker, &from, &tmp.path().join("elsewhere")).unwrap());
apply_custom_migration_in(&marker, &to);
assert!(!marker.exists());
assert!(from.join("lsp").exists());
assert!(!to.join("lsp").exists());
}
#[test]
fn custom_entries_cover_logs_and_agent_tools() {
let tmp = tempfile::tempdir().unwrap();
let entries = custom_entries(tmp.path());
for expected in ["logs", "agents/tools", "index", "cache", "lsp", "agents/registry.json"] {
assert!(entries.contains(&PathBuf::from(expected)), "{entries:?}");
}
}
}
+53
Переглянути файл
@@ -1,8 +1,12 @@
//! File › Invalidate Caches… and restarting the IDE.
use crate::state::AppState;
use crate::windows::WindowsState;
use ide_core::IdeResult;
use ide_host::data_dirs;
use ide_workspace::caches::{self, InvalidateCachesOptions};
use serde::Serialize;
use std::path::PathBuf;
use tauri::{AppHandle, Manager, State};
/// Schedules the invalidation for the next start (`None` cancels a pending one).
@@ -26,3 +30,52 @@ pub fn app_restart(app: AppHandle, windows: State<'_, WindowsState>) {
windows.set_restart();
super::window::app_quit(app, windows);
}
/// Settings › System Settings › Caches: where the local data lives.
#[derive(Debug, Clone, Serialize, specta::Type)]
#[serde(rename_all = "camelCase")]
pub struct LocalDataInfo {
/// The directory in use by this run.
pub current: String,
/// The OS default (what clearing the custom directory returns to).
pub default_dir: String,
/// The custom root stored by Settings, if any (`current` is `<root>/<identifier>`).
pub custom_root: Option<String>,
/// `OPENIDEA_LOCAL_DATA_DIR` is set and wins over everything Settings stores.
pub env_override: bool,
}
#[tauri::command]
#[specta::specta]
pub fn caches_local_data_info(app: AppHandle, state: State<'_, AppState>) -> IdeResult<LocalDataInfo> {
let identifier = app.config().identifier.clone();
let stringify = |path: PathBuf| path.display().to_string();
Ok(LocalDataInfo {
current: stringify(state.host.local_data_dir.clone()),
default_dir: stringify(data_dirs::default_local_data_dir(&identifier).map_err(ide_core::IdeError::internal)?),
custom_root: data_dirs::stored_override(&identifier).map(stringify),
env_override: std::env::var_os(data_dirs::LOCAL_DATA_ENV).is_some_and(|v| !v.is_empty()),
})
}
/// Stores (`Some`) or clears (`None`, an empty string counts) the custom
/// local-data root; with `migrate`, the current tree moves at the next start.
/// Returns the directory that takes effect after a restart. The caller
/// restarts the IDE (see `app_restart`).
#[tauri::command]
#[specta::specta]
pub fn caches_set_local_data_dir(
app: AppHandle,
state: State<'_, AppState>,
path: Option<String>,
migrate: bool,
) -> IdeResult<String> {
let identifier = app.config().identifier.clone();
let current = state.host.local_data_dir.clone();
let root = path.filter(|s| !s.trim().is_empty()).map(PathBuf::from);
let next = data_dirs::set_override(&identifier, root.as_deref()).map_err(ide_core::IdeError::internal)?;
if migrate {
data_dirs::schedule_custom_migration(&identifier, &current, &next).map_err(ide_core::IdeError::internal)?;
}
Ok(next.display().to_string())
}
+2
Переглянути файл
@@ -224,6 +224,8 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
refactor::refactor_undo_file_move,
refactor::refactor_can_undo_file_move,
caches::caches_schedule_invalidation,
caches::caches_local_data_info,
caches::caches_set_local_data_dir,
caches::app_restart,
nav::index_status,
nav::index_file_extensions,
+3 -1
Переглянути файл
@@ -90,7 +90,9 @@ pub fn run() {
linux_webkit::configure();
let refresh_shell_path = ide_host::shell_env::inherit_login_shell_path(&context.config().identifier);
let local_data_dir = ide_host::data_dirs::local_data_dir(&context.config().identifier);
// A custom directory from Settings › System Settings › Caches applies here too
// (`OPENIDEA_LOCAL_DATA_DIR` still wins), so tools resolve against the new root.
let local_data_dir = ide_host::data_dirs::startup_local_data_dir(&context.config().identifier);
// Managed LSP/DAP installs; must run before other threads start and before PATH is cached.
match &local_data_dir {
Ok(data) => {
+5
Переглянути файл
@@ -75,6 +75,11 @@ impl AppState {
tracing::info!(?data_dir, ?local_data_dir, "application data directories");
// Saved git HTTPS credentials (askpass answers them silently).
ide_ssh::git_creds::init(data_dir.join("git-credentials.json"));
// A newly chosen local-data directory from Settings › System Settings ›
// Caches: the previous tree moves first, so a pending invalidation still
// wipes what was just moved.
let identifier = app.config().identifier.clone();
ide_host::data_dirs::apply_custom_migration(&identifier, &local_data_dir);
// File › Invalidate Caches… requested at the last run.
let removed = ide_workspace::caches::apply_pending(&data_dir, &local_data_dir);
if !removed.is_empty() {
+20
Переглянути файл
@@ -406,6 +406,14 @@ export const commands = {
/** Build tool sync results (Maven / Gradle) and other per-project caches. */
buildCaches?: boolean,
} | null) => typedError<null, IdeError>(__TAURI_INVOKE("caches_schedule_invalidation", { options })),
cachesLocalDataInfo: () => typedError<LocalDataInfo, IdeError>(__TAURI_INVOKE("caches_local_data_info")),
/**
* Stores (`Some`) or clears (`None`, an empty string counts) the custom
* local-data root; with `migrate`, the current tree moves at the next start.
* Returns the directory that takes effect after a restart. The caller
* restarts the IDE (see `app_restart`).
*/
cachesSetLocalDataDir: (path: string | null, migrate: boolean) => typedError<string, IdeError>(__TAURI_INVOKE("caches_set_local_data_dir", { path, migrate })),
/** Quits through every window's unsaved-changes check, then starts again. */
appRestart: () => __TAURI_INVOKE<void>("app_restart"),
indexStatus: () => typedError<IndexStatus, IdeError>(__TAURI_INVOKE("index_status")),
@@ -6473,6 +6481,18 @@ export type LineMatch = {
textOffset: number,
};
/** Settings › System Settings › Caches: where the local data lives. */
export type LocalDataInfo = {
/** The directory in use by this run. */
current: string,
/** The OS default (what clearing the custom directory returns to). */
defaultDir: string,
/** The custom root stored by Settings, if any (`current` is `<root>/<identifier>`). */
customRoot: string | null,
/** `OPENIDEA_LOCAL_DATA_DIR` is set and wins over everything Settings stores. */
envOverride: boolean,
};
/** What a local definition declares (nvim-treesitter `@local.definition.<kind>`). */
export type LocalKind = "var" | "parameter" | "function" | "type" | "import" | "macro" | "label" |
/** Plain `@local.definition` or a kind without a better mapping. */
+133 -2
Переглянути файл
@@ -10,11 +10,13 @@ import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from "rea
import clsx from "clsx";
import { shortcutLabel } from "@/platform/keymap";
import { usePlatform } from "@/platform/platformInfo";
import { isTauri } from "@/platform/ipc";
import { call, commands, isTauri } from "@/platform/ipc";
import type { LocalDataInfo } from "@/bindings";
import { notifications } from "@/workbench/contributions";
import { ZOOM_LIMITS } from "@/platform/zoom";
import { SCHEMES as EDITOR_SCHEMES, listSchemes, customSchemes } from "@/platform/colorScheme";
import { formatTimestamp } from "@/platform/dateFormat";
import { Checkbox, IconButton, Input, Popup, type SelectOption, T } from "@/ui";
import { Button, Checkbox, IconButton, Input, Popup, type SelectOption, T } from "@/ui";
import { NOTIFICATION_GROUPS } from "@/workbench/notificationStore";
import { ColorSwatches, FormGroup, NumberInput, Row, SearchLabel, Select, Toggle } from "../components/Form";
import { useLabelMatch, useSetting } from "../components/context";
@@ -467,6 +469,124 @@ function DefaultDirectoryRow() {
);
}
/**
* Settings › System Settings › Caches: where tools, language servers, indexes
* and caches live (the C: problem of #1722). A custom directory takes effect
* after a restart; the previous tree moves along when asked.
*/
function LocalDataDirRow() {
const [info, setInfo] = useState<LocalDataInfo | null>(null);
const [root, setRoot] = useState("");
const [move, setMove] = useState(true);
const [busy, setBusy] = useState(false);
const id = useId();
useEffect(() => {
if (!isTauri()) return;
let live = true;
void call(commands.cachesLocalDataInfo()).then(
(v) => {
if (!live) return;
setInfo(v);
setRoot(v.customRoot ?? "");
},
() => {},
);
return () => {
live = false;
};
}, []);
const browse = async () => {
const { open } = await import("@/platform/dialog");
const picked = await open({
directory: true,
multiple: false,
title: "Local data directory",
defaultPath: root || undefined,
});
const path = Array.isArray(picked) ? picked[0] : picked;
if (path) setRoot(path);
};
const apply = async (next: string | null) => {
setBusy(true);
try {
await call(commands.cachesSetLocalDataDir(next, move));
await commands.appRestart();
} catch (e) {
notifications().notify({
level: "error",
title: "Could not change the local data directory",
body: e instanceof Error ? e.message : String(e),
key: "app.localDataDir",
});
} finally {
setBusy(false);
}
};
if (!isTauri()) {
return (
<p className={styles.note}>
<T>Restart is only available in the desktop app</T>
</p>
);
}
const locked = busy || info?.envOverride === true;
return (
<>
<Row label="Current location">
<Input className={styles.pathInput} value={info?.current ?? ""} readOnly />
</Row>
<Row
label="Custom directory"
htmlFor={id}
hint="Tools, language servers, indexes and caches live here. Empty means the OS default. Changing it restarts the IDE."
>
<Input
id={id}
className={styles.pathInput}
value={root}
placeholder="System default"
disabled={locked}
onChange={(e) => setRoot(e.target.value)}
/>
<IconButton
icon="folder-open"
size="small"
title="Browse…"
aria-label="Browse"
disabled={locked}
onClick={() => void browse()}
/>
</Row>
{info?.envOverride === true && (
<p role="note" className={styles.note}>
<T>The OPENIDEA_LOCAL_DATA_DIR environment variable is set and takes precedence.</T>
</p>
)}
<div className={styles.fontRow}>
<Button
variant="primary"
size="small"
disabled={locked}
onClick={() => void apply(root.trim() ? root.trim() : null)}
>
Apply and Restart
</Button>
<Button
size="small"
disabled={locked || (!root && !info?.customRoot)}
onClick={() => {
setRoot("");
void apply(null);
}}
>
Reset to default
</Button>
<Checkbox label="Move existing data to the new location" checked={move} onChange={setMove} disabled={locked} />
</div>
</>
);
}
function IdleSaveRow() {
const [on, setOn] = useSetting<boolean>("general.saveOnIdle");
const [seconds, setSeconds] = useSetting<number>("general.idleSeconds");
@@ -715,6 +835,17 @@ export const BEHAVIOR_PAGES: PageSpec[] = [
{ kind: "custom", label: "Default directory", component: DefaultDirectoryRow },
],
},
{
title: "Caches",
options: [
{
kind: "custom",
label: "Local data directory",
keywords: ["cache", "local data", "data directory", "C盘", "缓存", "目录"],
component: LocalDataDirRow,
},
],
},
{
title: "Autosave",
options: [
+10
Переглянути файл
@@ -7673,6 +7673,16 @@
"Featured": "精选",
"All Plugins": "全部插件",
"Examples": "示例插件",
"Caches": "缓存",
"Local data directory": "本地数据目录",
"Current location": "当前位置",
"Custom directory": "自定义目录",
"Tools, language servers, indexes and caches live here. Empty means the OS default. Changing it restarts the IDE.": "工具、语言服务器、索引和缓存存放在此。留空则使用系统默认位置。修改后将重启 IDE。",
"The OPENIDEA_LOCAL_DATA_DIR environment variable is set and takes precedence.": "已设置 OPENIDEA_LOCAL_DATA_DIR 环境变量,其优先级更高。",
"Apply and Restart": "应用并重启",
"Reset to default": "恢复默认",
"Move existing data to the new location": "将现有数据移动到新位置",
"Could not change the local data directory": "无法更改本地数据目录"
"Settings › Tools › SSH Configurations": "设置 › 工具 › SSH 配置",
"Edit SSH Configurations": "编辑 SSH 配置",
"Docker over SSH uses the system ssh: choose a configuration with OpenSSH config authentication.": "通过 SSH 连接 Docker 使用系统 ssh:请选择认证方式为“OpenSSH 配置和身份验证代理”的 SSH 配置。"