дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
feat(settings): import JetBrains settings.zip (colour schemes, editor font and options) (#1709)
Import Settings… also accepts a JetBrains settings.zip: colour schemes (and the current one), editor font, size, line spacing and ligatures, trailing-space stripping, final newline, sticky lines, whitespace, soft wraps and confirm-on-exit are mapped after a confirmation; keymaps, code styles, live templates, inspections and plugins are listed as not imported. Refs #656
Цей коміт міститься в:
1 джерело
309bb48f2c
коміт
1ab4edc0d7
11 files changed
+461
-2
No files matched your search
@@ -22,8 +22,8 @@ tracing.workspace = true
|
||||
tempfile.workspace = true
|
||||
globset.workspace = true
|
||||
uuid.workspace = true
|
||||
zip.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
encoding_rs.workspace = true
|
||||
zip.workspace = true
|
||||
@@ -316,6 +316,48 @@ impl SettingsStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// A JetBrains IDE's File › Manage IDE Settings › Export Settings… archive: every
|
||||
/// entry name, and the text of the entries OpenIDEA maps (the frontend does the mapping).
|
||||
#[derive(serde::Serialize, specta::Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JetBrainsSettings {
|
||||
pub entries: Vec<String>,
|
||||
/// Entry name → text, for `colors/*.icls` and the `options/*.xml` files OpenIDEA reads.
|
||||
pub files: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
const JETBRAINS_MARKER: &str = "IntelliJ IDEA Global Settings";
|
||||
const JETBRAINS_OPTIONS: [&str; 4] =
|
||||
["options/editor-font.xml", "options/colors.scheme.xml", "options/editor.xml", "options/ide.general.xml"];
|
||||
const JETBRAINS_MAX_TEXT: u64 = 4 << 20;
|
||||
|
||||
pub fn read_jetbrains_settings(path: &Path) -> IdeResult<JetBrainsSettings> {
|
||||
use std::io::Read;
|
||||
let not_jetbrains = || IdeError::invalid("not a JetBrains settings archive");
|
||||
let mut archive = zip::ZipArchive::new(std::fs::File::open(path)?).map_err(|_| not_jetbrains())?;
|
||||
let mut entries = Vec::with_capacity(archive.len());
|
||||
let mut files = BTreeMap::new();
|
||||
for i in 0..archive.len() {
|
||||
let entry = archive.by_index(i).map_err(|_| not_jetbrains())?;
|
||||
let name = entry.name().map_err(|_| not_jetbrains())?.into_owned();
|
||||
let wanted = JETBRAINS_OPTIONS.contains(&name.as_str())
|
||||
|| name.strip_prefix("colors/").is_some_and(|n| !n.contains('/') && n.ends_with(".icls"));
|
||||
if wanted {
|
||||
let mut text = String::new();
|
||||
entry
|
||||
.take(JETBRAINS_MAX_TEXT)
|
||||
.read_to_string(&mut text)
|
||||
.map_err(|e| IdeError::invalid(format!("{name}: {e}")))?;
|
||||
files.insert(name.clone(), text);
|
||||
}
|
||||
entries.push(name);
|
||||
}
|
||||
if !entries.iter().any(|e| e == JETBRAINS_MARKER) {
|
||||
return Err(not_jetbrains());
|
||||
}
|
||||
Ok(JetBrainsSettings { entries, files })
|
||||
}
|
||||
|
||||
const BUNDLE_FORMAT: &str = "openidea-settings";
|
||||
const BUNDLE_VERSION: u32 = 1;
|
||||
|
||||
@@ -546,6 +588,38 @@ mod tests {
|
||||
assert_eq!(target.effective().editor.font_size, 17.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_jetbrains_settings_archives() {
|
||||
use std::io::Write;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let write = |name: &str, entries: &[(&str, &str)]| {
|
||||
let path = tmp.path().join(name);
|
||||
let mut zip = zip::ZipWriter::new(std::fs::File::create(&path).unwrap());
|
||||
for (entry, text) in entries {
|
||||
zip.start_file(*entry, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
zip.write_all(text.as_bytes()).unwrap();
|
||||
}
|
||||
zip.finish().unwrap();
|
||||
path
|
||||
};
|
||||
let ok = write(
|
||||
"settings.zip",
|
||||
&[
|
||||
(JETBRAINS_MARKER, ""),
|
||||
("colors/_@user_Darcula.icls", "<scheme name=\"_@user_Darcula\"/>"),
|
||||
("options/editor-font.xml", "<application/>"),
|
||||
("options/vcs.xml", "<application/>"),
|
||||
("keymaps/macOS.xml", "<keymap/>"),
|
||||
],
|
||||
);
|
||||
let read = read_jetbrains_settings(&ok).unwrap();
|
||||
assert_eq!(read.entries.len(), 5);
|
||||
assert_eq!(read.files.keys().collect::<Vec<_>>(), ["colors/_@user_Darcula.icls", "options/editor-font.xml"]);
|
||||
assert!(read_jetbrains_settings(&write("other.zip", &[("options/editor.xml", "")])).is_err());
|
||||
std::fs::write(tmp.path().join("plain.zip"), "not a zip").unwrap();
|
||||
assert!(read_jetbrains_settings(&tmp.path().join("plain.zip")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main_toolbar_can_be_set_and_reset() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -237,6 +237,15 @@ pub async fn settings_import(window: Window, state: State<'_, AppState>, path: P
|
||||
Ok(SettingsImport { settings, storage })
|
||||
}
|
||||
|
||||
/// Import Settings… with a JetBrains `settings.zip`: reads it; the frontend maps and applies it.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn settings_read_jetbrains(path: PathBuf) -> IdeResult<ide_workspace::settings::JetBrainsSettings> {
|
||||
tauri::async_runtime::spawn_blocking(move || ide_workspace::settings::read_jetbrains_settings(&path))
|
||||
.await
|
||||
.map_err(|e| IdeError::internal(e.to_string()))?
|
||||
}
|
||||
|
||||
/// Effective source / test / resource / excluded roots of the window's project:
|
||||
/// auto-detected from build files, overridden by Settings › Project Structure.
|
||||
#[tauri::command]
|
||||
|
||||
@@ -98,6 +98,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
app::settings_set,
|
||||
app::settings_export,
|
||||
app::settings_import,
|
||||
app::settings_read_jetbrains,
|
||||
app::project_rename,
|
||||
app::project_roots,
|
||||
app::cancel_request,
|
||||
|
||||
@@ -81,6 +81,8 @@ export const commands = {
|
||||
settingsExport: (path: string, storage: { [key in string]: string }) => typedError<null, IdeError>(__TAURI_INVOKE("settings_export", { path, storage })),
|
||||
/** Import Settings…: replaces the global settings with the file's; every window re-reads them. */
|
||||
settingsImport: (path: string) => typedError<SettingsImport_Serialize, IdeError>(__TAURI_INVOKE("settings_import", { path })),
|
||||
/** Import Settings… with a JetBrains `settings.zip`: reads it; the frontend maps and applies it. */
|
||||
settingsReadJetbrains: (path: string) => typedError<JetBrainsSettings, IdeError>(__TAURI_INVOKE("settings_read_jetbrains", { path })),
|
||||
/**
|
||||
* File › Rename Project…: the project's display name (title bar, recent
|
||||
* projects); an empty `name` restores the folder name. Stored in the project's settings.
|
||||
@@ -4919,6 +4921,16 @@ export type JdkVendor = {
|
||||
name: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* A JetBrains IDE's File › Manage IDE Settings › Export Settings… archive: every
|
||||
* entry name, and the text of the entries OpenIDEA maps (the frontend does the mapping).
|
||||
*/
|
||||
export type JetBrainsSettings = {
|
||||
entries: string[],
|
||||
/** Entry name → text, for `colors/*.icls` and the `options/*.xml` files OpenIDEA reads. */
|
||||
files: { [key in string]: string },
|
||||
};
|
||||
|
||||
export type JvmFilePresentation = {
|
||||
path: string,
|
||||
presentation: JvmPresentation,
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { planJetBrainsImport } from "../jetbrainsSettings";
|
||||
|
||||
const icls = (name: string) =>
|
||||
`<scheme name="${name}" version="142" parent_scheme="Darcula"><attributes>` +
|
||||
`<option name="DEFAULT_KEYWORD"><value><option name="FOREGROUND" value="cc856b" /></value></option>` +
|
||||
`</attributes></scheme>`;
|
||||
|
||||
const component = (name: string, options: Record<string, string>) =>
|
||||
`<application><component name="${name}">${Object.entries(options)
|
||||
.map(([k, v]) => `<option name="${k}" value="${v}" />`)
|
||||
.join("")}</component></application>`;
|
||||
|
||||
const editor = { fontSize: 13, customSchemes: {}, colorOverrides: {} };
|
||||
|
||||
/** A WebStorm export, trimmed to what the import reads. */
|
||||
const webstorm = {
|
||||
entries: [
|
||||
"IntelliJ IDEA Global Settings",
|
||||
"colors/_@user_Darcula.icls",
|
||||
"options/editor-font.xml",
|
||||
"options/colors.scheme.xml",
|
||||
"options/editor.xml",
|
||||
"options/ide.general.xml",
|
||||
"options/vcs.xml",
|
||||
"keymaps/macOS.xml",
|
||||
"codestyles/Default.xml",
|
||||
"inspection/Default.xml",
|
||||
"installed.txt",
|
||||
],
|
||||
files: {
|
||||
"colors/_@user_Darcula.icls": icls("_@user_Darcula"),
|
||||
"options/colors.scheme.xml":
|
||||
'<application><component name="EditorColorsManagerImpl"><global_color_scheme name="_@user_Darcula" /></component></application>',
|
||||
"options/editor-font.xml": component("DefaultFont", {
|
||||
FONT_SIZE: "17",
|
||||
FONT_FAMILY: "JetBrains Maple Mono",
|
||||
USE_LIGATURES: "true",
|
||||
}),
|
||||
"options/editor.xml": component("EditorSettings", {
|
||||
USE_SOFT_WRAPS: "CONSOLE",
|
||||
STRIP_TRAILING_SPACES: "Whole",
|
||||
IS_ENSURE_NEWLINE_AT_EOF: "true",
|
||||
SHOW_STICKY_LINES: "false",
|
||||
}),
|
||||
"options/ide.general.xml": component("GeneralSettings", {
|
||||
showTipsOnStartup: "false",
|
||||
confirmExit: "false",
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
describe("planJetBrainsImport", () => {
|
||||
it("maps a WebStorm export to the settings it has, and lists what is left out", () => {
|
||||
const plan = planJetBrainsImport(webstorm, editor);
|
||||
const writes = Object.fromEntries(plan.writes);
|
||||
const schemes = writes["editor.customSchemes"] as Record<string, { name: string; parent: string }>;
|
||||
const [id, scheme] = Object.entries(schemes)[0]!;
|
||||
expect(scheme).toMatchObject({ name: "Darcula", parent: "dark" });
|
||||
expect(writes).toEqual({
|
||||
"editor.customSchemes": schemes,
|
||||
"editor.colorScheme": id,
|
||||
"editor.fontFamily": "JetBrains Maple Mono",
|
||||
"editor.fontSize": 17,
|
||||
"editor.ligatures": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"editor.general.stripTrailingScope": "all",
|
||||
"files.ensureFinalNewline": true,
|
||||
"editor.stickyScroll": false,
|
||||
"editor.softWrap": false,
|
||||
"general.confirmExit": false,
|
||||
});
|
||||
expect(plan.rows).toContainEqual({
|
||||
label: "Color Scheme",
|
||||
value: "Darcula",
|
||||
});
|
||||
expect(plan.rows).toContainEqual({ label: "Size", value: "17" });
|
||||
expect(plan.rows).toContainEqual({
|
||||
label: "Remove trailing spaces on:",
|
||||
value: "All lines",
|
||||
});
|
||||
expect(plan.skipped).toEqual(["Keymap", "Code Style", "Inspections", "Plugins"]);
|
||||
expect(plan.invalid).toEqual([]);
|
||||
});
|
||||
|
||||
it("writes only the keys present, and derives the line height from IDEA's spacing", () => {
|
||||
const plan = planJetBrainsImport(
|
||||
{
|
||||
entries: [],
|
||||
files: {
|
||||
"options/editor-font.xml": component("DefaultFont", {
|
||||
LINE_SPACING: "1.5",
|
||||
}),
|
||||
"options/editor.xml": component("EditorSettings", {
|
||||
USE_SOFT_WRAPS: "MAIN_EDITOR,CONSOLE",
|
||||
STRIP_TRAILING_SPACES: "None",
|
||||
}),
|
||||
},
|
||||
},
|
||||
editor,
|
||||
);
|
||||
expect(plan.writes).toEqual([
|
||||
["editor.lineHeight", 20],
|
||||
["files.trimTrailingWhitespace", false],
|
||||
["editor.softWrap", true],
|
||||
]);
|
||||
expect(plan.skipped).toEqual([]);
|
||||
});
|
||||
|
||||
it("re-importing replaces the earlier copy of a scheme and drops its edits", () => {
|
||||
const first = Object.fromEntries(planJetBrainsImport(webstorm, editor).writes);
|
||||
const customs = first["editor.customSchemes"] as typeof editor.customSchemes;
|
||||
const [id] = Object.keys(customs);
|
||||
const again = planJetBrainsImport(webstorm, {
|
||||
...editor,
|
||||
customSchemes: customs,
|
||||
colorOverrides: { [id!]: { keyword: { bold: true } }, other: {} },
|
||||
});
|
||||
const writes = Object.fromEntries(again.writes);
|
||||
expect(Object.keys(writes["editor.customSchemes"] as object)).toEqual([id]);
|
||||
expect(writes["editor.colorOverrides"]).toEqual({ other: {} });
|
||||
expect(writes["editor.colorScheme"]).toBe(id);
|
||||
});
|
||||
|
||||
it("skips colour schemes that cannot be read", () => {
|
||||
const plan = planJetBrainsImport({ entries: [], files: { "colors/broken.icls": "<nope" } }, editor);
|
||||
expect(plan.invalid).toEqual(["broken.icls"]);
|
||||
expect(plan.writes).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -214,3 +214,24 @@
|
||||
font-size: var(--fs-small);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* Import Settings… › JetBrains settings.zip confirmation */
|
||||
.importSummary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.importList {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 2px 16px;
|
||||
}
|
||||
.importList dt {
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
.importList dd {
|
||||
margin: 0;
|
||||
color: var(--fg-strong);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Import Settings… with a JetBrains `settings.zip`: maps the archive's colour
|
||||
* schemes and the editor / general options OpenIDEA has to settings writes, after
|
||||
* a confirmation listing them. Only keys present in the archive are written
|
||||
* (JetBrains leaves defaults out). Keymaps, code styles and the rest are listed
|
||||
* as not imported.
|
||||
*/
|
||||
import { Fragment } from "react";
|
||||
import type { JetBrainsSettings } from "@/bindings";
|
||||
import { customSchemes } from "@/platform/colorScheme";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { importScheme, parseIcls } from "@/platform/icls";
|
||||
import { call, commands } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { ask } from "@/workbench/messages";
|
||||
import styles from "./app.module.css";
|
||||
|
||||
export interface JetBrainsImportPlan {
|
||||
/** Settings writes, in order: `settingsSet("global", key, JSON.stringify(value))`. */
|
||||
writes: [key: string, value: unknown][];
|
||||
/** What the confirmation lists (translated): the setting and its imported value. */
|
||||
rows: { label: string; value: string }[];
|
||||
/** Categories present in the archive that are not imported (translated). */
|
||||
skipped: string[];
|
||||
/** `.icls` files that could not be read. */
|
||||
invalid: string[];
|
||||
}
|
||||
|
||||
interface EditorState {
|
||||
fontSize: number | null;
|
||||
customSchemes: unknown;
|
||||
colorOverrides: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const USER_PREFIX = "_@user_";
|
||||
|
||||
/**
|
||||
* `<option name value>` of `<component name>`; empty when the file or component is
|
||||
* absent. With `tag`, the `name` of that child element instead, under key "".
|
||||
*/
|
||||
function options(xml: string | undefined, component: string, tag?: string): Map<string, string> {
|
||||
const out = new Map<string, string>();
|
||||
if (!xml) return out;
|
||||
const doc = new DOMParser().parseFromString(xml, "application/xml");
|
||||
if (doc.querySelector("parsererror")) return out;
|
||||
for (const c of Array.from(doc.documentElement.children))
|
||||
if (c.tagName === "component" && c.getAttribute("name") === component)
|
||||
for (const o of Array.from(c.children)) {
|
||||
const name = o.getAttribute("name");
|
||||
const value = o.getAttribute("value");
|
||||
if (tag && o.tagName === tag && name !== null) out.set("", name);
|
||||
else if (!tag && o.tagName === "option" && name && value !== null) out.set(name, value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const bool = (v: string | undefined) => (v === undefined ? undefined : v === "true");
|
||||
const num = (v: string | undefined) => {
|
||||
const n = v === undefined ? NaN : Number(v);
|
||||
return Number.isFinite(n) && n > 0 ? n : undefined;
|
||||
};
|
||||
|
||||
const onOff = (v: boolean) => (v ? tx("On") : tx("Off"));
|
||||
|
||||
export function planJetBrainsImport(bundle: JetBrainsSettings, editor: EditorState): JetBrainsImportPlan {
|
||||
const plan: JetBrainsImportPlan = { writes: [], rows: [], skipped: [], invalid: [] };
|
||||
const add = (key: string, value: string | number | boolean | undefined, label: string, shown?: string) => {
|
||||
if (value === undefined) return;
|
||||
plan.writes.push([key, value]);
|
||||
plan.rows.push({ label, value: shown ?? (typeof value === "boolean" ? onOff(value) : String(value)) });
|
||||
};
|
||||
|
||||
let customs = customSchemes(editor.customSchemes);
|
||||
let overrides = editor.colorOverrides;
|
||||
const imported = new Map<string, string>(); // name in the .icls → scheme id
|
||||
for (const [file, text] of Object.entries(bundle.files)) {
|
||||
if (!file.startsWith("colors/")) continue;
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseIcls(text);
|
||||
} catch {
|
||||
plan.invalid.push(file.slice("colors/".length));
|
||||
continue;
|
||||
}
|
||||
const original = parsed.scheme.name;
|
||||
const name = original.startsWith(USER_PREFIX) ? original.slice(USER_PREFIX.length) : original;
|
||||
// Overwrites an earlier import of the same scheme instead of adding "Name (2)".
|
||||
const next = importScheme(customs, { ...parsed.scheme, name }, true);
|
||||
customs = next.customs;
|
||||
if (overrides[next.id]) overrides = Object.fromEntries(Object.entries(overrides).filter(([id]) => id !== next.id));
|
||||
imported.set(original, next.id);
|
||||
plan.rows.push({ label: tx("Color Scheme"), value: customs[next.id]?.name ?? name });
|
||||
}
|
||||
if (imported.size) {
|
||||
plan.writes.push(["editor.customSchemes", customs]);
|
||||
if (overrides !== editor.colorOverrides) plan.writes.push(["editor.colorOverrides", overrides]);
|
||||
const global = options(bundle.files["options/colors.scheme.xml"], "EditorColorsManagerImpl", "global_color_scheme");
|
||||
const id = imported.get(global.get("") ?? "");
|
||||
if (id) add("editor.colorScheme", id, tx("Current color scheme"), customs[id]?.name);
|
||||
}
|
||||
|
||||
const font = options(bundle.files["options/editor-font.xml"], "DefaultFont");
|
||||
add("editor.fontFamily", font.get("FONT_FAMILY")?.trim() || undefined, tx("Font"));
|
||||
const size = num(font.get("FONT_SIZE"));
|
||||
add("editor.fontSize", size, tx("Size"));
|
||||
const spacing = num(font.get("LINE_SPACING"));
|
||||
const base = size ?? editor.fontSize;
|
||||
add("editor.lineHeight", spacing && base ? Math.round(base * spacing) : undefined, tx("Line height"));
|
||||
add("editor.ligatures", bool(font.get("USE_LIGATURES")), tx("Enable ligatures"));
|
||||
|
||||
const ed = options(bundle.files["options/editor.xml"], "EditorSettings");
|
||||
const strip = ed.get("STRIP_TRAILING_SPACES"); // None | Changed | Whole
|
||||
if (strip === "Changed" || strip === "Whole") {
|
||||
const scope = strip === "Whole" ? "all" : "modified";
|
||||
plan.writes.push(["files.trimTrailingWhitespace", true]);
|
||||
add(
|
||||
"editor.general.stripTrailingScope",
|
||||
scope,
|
||||
tx("Remove trailing spaces on:"),
|
||||
tx(strip === "Whole" ? "All lines" : "Modified lines"),
|
||||
);
|
||||
} else if (strip === "None") add("files.trimTrailingWhitespace", false, tx("Remove trailing spaces on:"));
|
||||
add(
|
||||
"files.ensureFinalNewline",
|
||||
bool(ed.get("IS_ENSURE_NEWLINE_AT_EOF")),
|
||||
tx("Ensure every saved file ends with a line break"),
|
||||
);
|
||||
add("editor.stickyScroll", bool(ed.get("SHOW_STICKY_LINES")), tx("Show sticky lines while scrolling"));
|
||||
add("editor.showWhitespace", bool(ed.get("IS_WHITESPACES_SHOWN")), tx("Show whitespaces"));
|
||||
// A comma-separated list of places: MAIN_EDITOR, CONSOLE, PREVIEW.
|
||||
const wraps = ed.get("USE_SOFT_WRAPS");
|
||||
add(
|
||||
"editor.softWrap",
|
||||
wraps === undefined ? undefined : wraps.split(",").includes("MAIN_EDITOR"),
|
||||
tx("Soft-wrap files"),
|
||||
);
|
||||
|
||||
const general = options(bundle.files["options/ide.general.xml"], "GeneralSettings");
|
||||
add("general.confirmExit", bool(general.get("confirmExit")), tx("Confirm before exiting the IDE"));
|
||||
|
||||
const has = (prefix: string) => bundle.entries.some((e) => e.startsWith(prefix));
|
||||
if (has("keymaps/")) plan.skipped.push(tx("Keymap"));
|
||||
if (has("codestyles/")) plan.skipped.push(tx("Code Style"));
|
||||
if (has("templates/")) plan.skipped.push(tx("Live templates"));
|
||||
if (has("inspection/")) plan.skipped.push(tx("Inspections"));
|
||||
if (has("installed.txt")) plan.skipped.push(tx("Plugins"));
|
||||
return plan;
|
||||
}
|
||||
|
||||
function Summary({ plan }: { plan: JetBrainsImportPlan }) {
|
||||
const left = [...plan.skipped, ...plan.invalid];
|
||||
return (
|
||||
<div className={styles.importSummary}>
|
||||
<div>{tx("These settings will be imported:")}</div>
|
||||
<dl className={styles.importList}>
|
||||
{plan.rows.map((row, i) => (
|
||||
<Fragment key={i}>
|
||||
<dt>{row.label}</dt>
|
||||
<dd>{row.value}</dd>
|
||||
</Fragment>
|
||||
))}
|
||||
</dl>
|
||||
{left.length > 0 && (
|
||||
<div>
|
||||
{tx("Not imported:")} {left.join(", ")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Reads `path`, asks, then writes the mapped settings to the global layer. */
|
||||
export async function importJetBrainsSettings(path: string) {
|
||||
const bundle = await call(commands.settingsReadJetbrains(path));
|
||||
const { editor } = await commands.settingsGetGlobal();
|
||||
const plan = planJetBrainsImport(bundle, editor);
|
||||
if (!plan.writes.length) {
|
||||
notifications().notify({
|
||||
level: "warning",
|
||||
title: "No settings OpenIDEA can import were found in this archive",
|
||||
key: "app.settingsImport",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { button } = await ask({
|
||||
title: tx("Import JetBrains Settings"),
|
||||
body: <Summary plan={plan} />,
|
||||
buttons: [{ id: "import", label: tx("Import"), primary: true }],
|
||||
width: 520,
|
||||
});
|
||||
if (button !== "import") return;
|
||||
let settings = useProject.getState().settings;
|
||||
for (const [key, value] of plan.writes)
|
||||
settings = await call(commands.settingsSet("global", key, JSON.stringify(value)));
|
||||
useProject.setState({ settings });
|
||||
notifications().notify({ level: "info", title: "Settings imported", body: path, key: "app.settingsImport" });
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { call, commands, isTauri } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { notifications, registerEditorBanner } from "@/workbench/contributions";
|
||||
import { promptInput } from "@/workbench/inputDialogs";
|
||||
import { importJetBrainsSettings } from "./jetbrainsSettings";
|
||||
import { showNewProject } from "./NewProject";
|
||||
import { tx } from "@/platform/i18n";
|
||||
|
||||
@@ -80,11 +81,12 @@ async function importSettings() {
|
||||
const picked = await open({
|
||||
title: "Import Settings",
|
||||
multiple: false,
|
||||
filters: [{ name: "OpenIDEA settings", extensions: ["json"] }],
|
||||
filters: [{ name: "OpenIDEA or JetBrains settings", extensions: ["json", "zip"] }],
|
||||
});
|
||||
const path = Array.isArray(picked) ? picked[0] : picked;
|
||||
if (!path) return;
|
||||
try {
|
||||
if (/\.zip$/i.test(path)) return await importJetBrainsSettings(path);
|
||||
const result = await call(commands.settingsImport(path));
|
||||
for (const [key, value] of Object.entries(result.storage))
|
||||
if (isExported(key))
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"OpenIDEA or JetBrains settings": "OpenIDEA or JetBrains settings",
|
||||
"Import JetBrains Settings": "Import JetBrains Settings",
|
||||
"These settings will be imported:": "These settings will be imported:",
|
||||
"Not imported:": "Not imported:",
|
||||
"Current color scheme": "Current color scheme",
|
||||
"No settings OpenIDEA can import were found in this archive": "No settings OpenIDEA can import were found in this archive",
|
||||
"Overwrite existing branch": "Overwrite existing branch",
|
||||
"Branch '{n}' already exists": "Branch '{n}' already exists",
|
||||
"Cannot copy reference": "Cannot copy reference",
|
||||
|
||||
@@ -3016,6 +3016,12 @@
|
||||
"Settings imported": "设置已导入",
|
||||
"Restart to apply imported templates, macros, layouts and view options.": "重启以应用导入的模板、宏、布局和视图选项。",
|
||||
"Cannot import settings": "无法导入设置",
|
||||
"OpenIDEA or JetBrains settings": "OpenIDEA 或 JetBrains 设置",
|
||||
"Import JetBrains Settings": "导入 JetBrains 设置",
|
||||
"These settings will be imported:": "将导入以下设置:",
|
||||
"Not imported:": "不导入:",
|
||||
"Current color scheme": "当前配色方案",
|
||||
"No settings OpenIDEA can import were found in this archive": "此压缩包中没有 OpenIDEA 可以导入的设置",
|
||||
"Rename Project": "重命名项目",
|
||||
"Rename Project…": "重命名项目…",
|
||||
"New project name:": "新项目名称:",
|
||||
|
||||
Reference in new issue
Заблокувати користувача