mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
feat(tools): offer updates for the language servers and debuggers the IDE installed (#3247)
Settings › Languages & Frameworks › Language Servers looks for newer versions of the tools in the IDE's tools folder once per session and from its refresh button: release downloads resolve their latest release as an install does, npm packages ask npm view through the user's registry. A tool with a newer version gets an Update tag, the available version in its details and an Update button that installs it.
This commit is contained in:
@@ -6,7 +6,7 @@ use crate::rpc::Dispatcher;
|
||||
use ide_core::{IdeError, IdeResult, RequestId};
|
||||
use ide_sdk::Sdk;
|
||||
use ide_tools::jdk::{JdkReleases, JdkVendor};
|
||||
use ide_tools::{InstallEvent, ToolInfo, Tools};
|
||||
use ide_tools::{InstallEvent, ToolInfo, ToolUpdate, Tools};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// The installer, reporting the executables chosen in the window's settings.
|
||||
@@ -89,6 +89,16 @@ pub async fn tools_install(
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// Newer versions of the tools the IDE installed (release downloads and npm packages).
|
||||
/// Cancel with `cancel_request(request)`.
|
||||
pub async fn tools_updates(host: &Host, window: &str, request: RequestId) -> IdeResult<Vec<ToolUpdate>> {
|
||||
let tools = tools(host, window)?;
|
||||
let cancel = host.begin_request(request);
|
||||
let result = tokio::task::spawn_blocking(move || tools.updates(&cancel)).await.map_err(join_err);
|
||||
host.end_request(request);
|
||||
result?
|
||||
}
|
||||
|
||||
/// Removes a tool the IDE installed.
|
||||
pub async fn tools_uninstall(host: &Host, window: &str, name: String) -> IdeResult<ToolInfo> {
|
||||
let tools = tools(host, window)?;
|
||||
@@ -185,6 +195,7 @@ pub(crate) fn register(d: &mut Dispatcher) {
|
||||
tools_install(&c.host, &c.window, c.arg("name")?, c.arg("request")?, c.sender("onEvent")?).await
|
||||
});
|
||||
d.add("tools_uninstall", |c| async move { tools_uninstall(&c.host, &c.window, c.arg("name")?).await });
|
||||
d.add("tools_updates", |c| async move { tools_updates(&c.host, &c.window, c.arg("request")?).await });
|
||||
d.add("jdk_download_vendors", |c| async move { Ok(jdk_download_vendors(&c.host, &c.window)) });
|
||||
d.add("jdk_download_releases", |c| async move {
|
||||
jdk_download_releases(&c.host, &c.window, c.arg("vendor")?, c.arg("request")?).await
|
||||
|
||||
@@ -104,6 +104,17 @@ pub enum InstallEvent {
|
||||
Progress { fraction: Option<f64>, message: String },
|
||||
}
|
||||
|
||||
/// A newer version of a tool the IDE installed, as its install source offers it now.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolUpdate {
|
||||
pub name: String,
|
||||
/// The installed version.
|
||||
pub current: String,
|
||||
/// The version installing the tool again would get.
|
||||
pub latest: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Manifest {
|
||||
version: Option<String>,
|
||||
@@ -1437,6 +1448,77 @@ impl Tools {
|
||||
self.write_manifest(spec.name, &Manifest { version: Some(dl.version.clone()), source: Some(dl.url.clone()) })
|
||||
}
|
||||
|
||||
/// Newer versions of the tools the IDE installed into its tools folder, for the install
|
||||
/// sources that can be asked: release downloads (GitHub, Open VSX, Maven Central, …) and
|
||||
/// npm packages. A tool whose lookup fails (offline, rate limited) is left out.
|
||||
pub fn updates(&self, cancel: &CancellationToken) -> IdeResult<Vec<ToolUpdate>> {
|
||||
let installed: Vec<(&ToolSpec, Method, String)> = TOOLS
|
||||
.iter()
|
||||
.filter_map(|spec| {
|
||||
let method = self.install_method(spec);
|
||||
Some((spec, method, self.installed_version(spec, method)?))
|
||||
})
|
||||
.collect();
|
||||
let updates = std::thread::scope(|scope| {
|
||||
let lookups: Vec<_> = installed
|
||||
.iter()
|
||||
.map(|(spec, method, current)| {
|
||||
scope.spawn(move || match self.for_method(*method).latest_version(*method, cancel) {
|
||||
Ok(Some(latest)) if latest != *current => {
|
||||
Some(ToolUpdate { name: spec.name.into(), current: current.clone(), latest })
|
||||
}
|
||||
Ok(_) => None,
|
||||
Err(e) => {
|
||||
tracing::info!("update check for {}: {}", spec.name, e.message);
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
lookups.into_iter().filter_map(|lookup| lookup.join().ok().flatten()).collect()
|
||||
});
|
||||
if cancel.is_cancelled() {
|
||||
return Err(IdeError::cancelled());
|
||||
}
|
||||
Ok(updates)
|
||||
}
|
||||
|
||||
/// The version of `spec` in the IDE's tools folder, when `method` (how it was installed)
|
||||
/// can tell the latest one.
|
||||
fn installed_version(&self, spec: &ToolSpec, method: Method) -> Option<String> {
|
||||
match method {
|
||||
Method::Release { .. } => {
|
||||
managed_bin_dir(&self.root, spec, self.platform).and_then(|dir| self.exe_in(&dir, spec.commands[0]))?;
|
||||
self.manifest(spec.name)?.version
|
||||
}
|
||||
// A pinned package (`typescript@6`) stays on its major version.
|
||||
Method::Npm { packages } if npm_name(packages[0]) == packages[0] => self.npm_package_version(packages[0]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The version installing with `method` would get now.
|
||||
fn latest_version(&self, method: Method, cancel: &CancellationToken) -> IdeResult<Option<String>> {
|
||||
match method {
|
||||
Method::Release { source, assets, .. } => {
|
||||
let http = Http::new(&self.system_path)?;
|
||||
Ok(Some(self.resolve_release(&http, source, assets, cancel)?.version))
|
||||
}
|
||||
Method::Npm { packages } => {
|
||||
let (manager, _) = self.npm_manager()?;
|
||||
let mut c = self.command(manager);
|
||||
// The registry the user's npm configuration names (a mirror, a proxy). A cold npm
|
||||
// on a slow registry takes several seconds per package, more when run side by side.
|
||||
c.args(["view", packages[0], "version"]).current_dir(&self.root);
|
||||
match run_quiet(c, Duration::from_secs(60)) {
|
||||
Some((true, out)) => Ok(out.lines().map(str::trim).rfind(|l| !l.is_empty()).map(String::from)),
|
||||
_ => Err(IdeError::unavailable(format!("cannot look up the latest {}", packages[0]))),
|
||||
}
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a tool the IDE installed.
|
||||
pub fn uninstall(&self, name: &str, langs: &[LanguageConfig]) -> IdeResult<ToolInfo> {
|
||||
match registry::find(name) {
|
||||
|
||||
@@ -935,3 +935,26 @@ fn a_cancelled_install_leaves_the_previous_one_in_place() {
|
||||
assert!(dest.join("new").exists() && !dest.join("old").exists());
|
||||
assert!(aside.join("old").exists(), "the previous install is moved aside");
|
||||
}
|
||||
|
||||
/// An npm tool the IDE installed is offered the version the user's npm registry has now.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn npm_tools_report_a_newer_registry_version() {
|
||||
let env = Env::new();
|
||||
let sdk = env._tmp.path().join("node-sdk/bin");
|
||||
let modules = env.root.join("node_modules");
|
||||
env.script(&modules.join(".bin"), "bash-language-server", "echo 5.4.0");
|
||||
fs::create_dir_all(modules.join("bash-language-server")).unwrap();
|
||||
fs::write(modules.join("bash-language-server/package.json"), r#"{"version":"5.4.0"}"#).unwrap();
|
||||
env.script(&sdk, "node", "exit 0");
|
||||
let tools = |latest: &str| {
|
||||
env.script(&sdk, "npm", &format!(r#"[ "$1 $2 $3" = "view bash-language-server version" ] && echo {latest}"#));
|
||||
env.tools().with_runtime(BTreeMap::from([("node".into(), sdk.join("node"))]), BTreeMap::new())
|
||||
};
|
||||
let cancel = CancellationToken::new();
|
||||
assert_eq!(
|
||||
tools("5.6.1").updates(&cancel).unwrap(),
|
||||
vec![ToolUpdate { name: "bash-language-server".into(), current: "5.4.0".into(), latest: "5.6.1".into() }]
|
||||
);
|
||||
assert_eq!(tools("5.4.0").updates(&cancel).unwrap(), vec![]);
|
||||
}
|
||||
|
||||
@@ -458,6 +458,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
tools::tools_list,
|
||||
tools::tools_install,
|
||||
tools::tools_uninstall,
|
||||
tools::tools_updates,
|
||||
tools::jdk_download_vendors,
|
||||
tools::jdk_download_releases,
|
||||
tools::jdk_download_location,
|
||||
|
||||
@@ -6,7 +6,7 @@ use ide_core::{IdeResult, RequestId};
|
||||
pub use ide_host::ops::tools::*;
|
||||
use ide_sdk::Sdk;
|
||||
use ide_tools::jdk::{JdkReleases, JdkVendor};
|
||||
use ide_tools::{InstallEvent, ToolInfo};
|
||||
use ide_tools::{InstallEvent, ToolInfo, ToolUpdate};
|
||||
use std::path::PathBuf;
|
||||
use tauri::ipc::Channel;
|
||||
use tauri::{State, Window};
|
||||
@@ -33,6 +33,18 @@ pub async fn tools_install(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Newer versions of the tools the IDE installed (release downloads and npm packages).
|
||||
/// Cancel with `cancel_request(request)`.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn tools_updates(
|
||||
window: Window,
|
||||
state: State<'_, AppState>,
|
||||
request: RequestId,
|
||||
) -> IdeResult<Vec<ToolUpdate>> {
|
||||
ide_host::ops::tools::tools_updates(&state.host, window.label(), request).await
|
||||
}
|
||||
|
||||
/// Removes a tool the IDE installed.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
|
||||
@@ -966,6 +966,11 @@ export const commands = {
|
||||
toolsInstall: (name: string, request: RequestId, onEvent: Channel<InstallEvent>) => typedError<ToolInfo, IdeError>(__TAURI_INVOKE("tools_install", { name, request, onEvent })),
|
||||
/** Removes a tool the IDE installed. */
|
||||
toolsUninstall: (name: string) => typedError<ToolInfo, IdeError>(__TAURI_INVOKE("tools_uninstall", { name })),
|
||||
/**
|
||||
* Newer versions of the tools the IDE installed (release downloads and npm packages).
|
||||
* Cancel with `cancel_request(request)`.
|
||||
*/
|
||||
toolsUpdates: (request: RequestId) => typedError<ToolUpdate[], IdeError>(__TAURI_INVOKE("tools_updates", { request })),
|
||||
/** JDK distributions Download JDK offers. */
|
||||
jdkDownloadVendors: () => __TAURI_INVOKE<JdkVendor[]>("jdk_download_vendors"),
|
||||
/** Java versions `vendor` publishes; cancellable with `cancel_request(request)`. */
|
||||
@@ -10222,6 +10227,15 @@ export type ToolStatus =
|
||||
/** The executable chosen in settings. */
|
||||
"custom" | "missing";
|
||||
|
||||
/** A newer version of a tool the IDE installed, as its install source offers it now. */
|
||||
export type ToolUpdate = {
|
||||
name: string,
|
||||
/** The installed version. */
|
||||
current: string,
|
||||
/** The version installing the tool again would get. */
|
||||
latest: string,
|
||||
};
|
||||
|
||||
export type TransferError = {
|
||||
path: string,
|
||||
message: string,
|
||||
|
||||
@@ -124,7 +124,8 @@
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
.rowName {
|
||||
flex: none;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 60%;
|
||||
overflow: hidden;
|
||||
color: var(--fg-strong);
|
||||
@@ -150,6 +151,13 @@
|
||||
font-size: var(--fs-tiny);
|
||||
line-height: 16px;
|
||||
}
|
||||
.rowBadge {
|
||||
flex: none;
|
||||
}
|
||||
.updateTag {
|
||||
background: color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
color: var(--accent);
|
||||
}
|
||||
.listEmpty {
|
||||
padding: 8px 12px;
|
||||
color: var(--fg-muted);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { languageNames, NewServerDialog, ServerSettings, ToolSettings, useLangua
|
||||
import { serverKey, type ServerEntry } from "./serverConfig";
|
||||
import {
|
||||
cancelInstall,
|
||||
checkToolUpdates,
|
||||
installTool,
|
||||
refreshTools,
|
||||
STATUS_LABEL,
|
||||
@@ -98,12 +99,14 @@ function ToolRow({
|
||||
selected,
|
||||
off,
|
||||
pending,
|
||||
update,
|
||||
onSelect,
|
||||
}: {
|
||||
tool: ToolInfo;
|
||||
selected: boolean;
|
||||
off: boolean;
|
||||
pending: boolean;
|
||||
update: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
return (
|
||||
@@ -116,7 +119,7 @@ function ToolRow({
|
||||
title={`${tool.displayName} — ${tool.languages.join(", ")}`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<Badge dot tone={pending ? "accent" : STATUS_TONE[tool.status]}>
|
||||
<Badge dot className={styles.rowBadge} tone={pending ? "accent" : STATUS_TONE[tool.status]}>
|
||||
{pending ? tx("New") : STATUS_LABEL[tool.status]}
|
||||
</Badge>
|
||||
<span className={styles.rowName}>{tool.displayName}</span>
|
||||
@@ -135,6 +138,11 @@ function ToolRow({
|
||||
<T>Disabled</T>
|
||||
</span>
|
||||
)}
|
||||
{update && (
|
||||
<span className={clsx(styles.kindTag, styles.updateTag)}>
|
||||
<T>Update</T>
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.rowLanguages}>{tool.languages.join(", ")}</span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -144,6 +152,7 @@ function ToolRow({
|
||||
function Details({ tool, pending }: { tool: ToolInfo; pending: boolean }) {
|
||||
const job = useTools((s) => s.jobs[tool.name]);
|
||||
const last = useTools((s) => s.logs[tool.name]);
|
||||
const update = useTools((s) => (tool.status === "installed" ? s.updates[tool.name] : undefined));
|
||||
const [logOpen, setLogOpen] = useState(false);
|
||||
const log = job?.lines ?? last?.lines ?? [];
|
||||
const lastError = !job && last?.error !== "Cancelled" ? last?.error : null;
|
||||
@@ -185,17 +194,21 @@ function Details({ tool, pending }: { tool: ToolInfo; pending: boolean }) {
|
||||
{tool.canInstall && (
|
||||
<Button
|
||||
size="small"
|
||||
variant={tool.status === "missing" ? "primary" : "secondary"}
|
||||
variant={tool.status === "missing" || update ? "primary" : "secondary"}
|
||||
disabled={busy}
|
||||
title={
|
||||
tool.installMethod ? tx`Installs into the IDE's own tools folder via ${tool.installMethod}` : undefined
|
||||
}
|
||||
onClick={() => void installTool(tool.name)}
|
||||
aria-label={
|
||||
tool.status === "installed" ? tx`Reinstall ${tool.displayName}` : tx`Install ${tool.displayName}`
|
||||
update
|
||||
? tx`Update ${tool.displayName}`
|
||||
: tool.status === "installed"
|
||||
? tx`Reinstall ${tool.displayName}`
|
||||
: tx`Install ${tool.displayName}`
|
||||
}
|
||||
>
|
||||
{tool.status === "installed" ? "Reinstall" : "Install"}
|
||||
{update ? "Update" : tool.status === "installed" ? "Reinstall" : "Install"}
|
||||
</Button>
|
||||
)}
|
||||
{tool.canUninstall && (
|
||||
@@ -205,6 +218,11 @@ function Details({ tool, pending }: { tool: ToolInfo; pending: boolean }) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{update && !job && (
|
||||
<div className={styles.notice} role="note">
|
||||
{tx`Version ${update.latest} is available.`}
|
||||
</div>
|
||||
)}
|
||||
{job && <JobRow name={tool.displayName} job={job} />}
|
||||
{lastError && (
|
||||
<div className={styles.error} role="alert">
|
||||
@@ -244,7 +262,7 @@ function Details({ tool, pending }: { tool: ToolInfo; pending: boolean }) {
|
||||
}
|
||||
|
||||
export function LanguageServersPage() {
|
||||
const { tools, loaded, loading, error } = useTools();
|
||||
const { tools, loaded, loading, error, updates, checkingUpdates } = useTools();
|
||||
const form = useSettingsForm();
|
||||
const [query, setQuery] = useState("");
|
||||
const [filter, setFilter] = useState<Filter>("all");
|
||||
@@ -257,6 +275,8 @@ export function LanguageServersPage() {
|
||||
|
||||
useEffect(() => {
|
||||
void refreshTools();
|
||||
// Asks each install source once per session; the refresh button asks again.
|
||||
if (!useTools.getState().updatesChecked) void checkToolUpdates();
|
||||
// Applied paths and servers change what is found and listed.
|
||||
return onIdeEvent("settingsChanged", (e) => {
|
||||
if (!e.key || /^(tools|languageServers)(\.|$)/.test(e.key)) void refreshTools();
|
||||
@@ -307,6 +327,7 @@ export function LanguageServersPage() {
|
||||
selected={t === selected}
|
||||
off={servers[t.name]?.enabled === false}
|
||||
pending={pending.has(t.name)}
|
||||
update={t.status === "installed" && !!updates[t.name]}
|
||||
onSelect={() => setSelectedName(t.name)}
|
||||
/>
|
||||
))}
|
||||
@@ -337,14 +358,17 @@ export function LanguageServersPage() {
|
||||
onClick={() => selected && form.set(serverKey(selected.name), null)}
|
||||
/>
|
||||
<span className={styles.toolbarSep} aria-hidden />
|
||||
{loading ? (
|
||||
{loading || checkingUpdates ? (
|
||||
<Spinner size={14} label="Checking tools" />
|
||||
) : (
|
||||
<IconButton
|
||||
icon="refresh"
|
||||
size="small"
|
||||
title="Re-check installed tools"
|
||||
onClick={() => void refreshTools()}
|
||||
onClick={() => {
|
||||
void refreshTools();
|
||||
void checkToolUpdates();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<SearchField
|
||||
|
||||
@@ -19,7 +19,17 @@ beforeEach(() => {
|
||||
provideNotificationService({ notify: (n) => void notes.push(n) });
|
||||
backend = new MemoryToolsBackend(sampleTools(), 1);
|
||||
setToolsBackend(backend);
|
||||
useTools.setState({ tools: [], loaded: false, loading: false, error: null, jobs: {}, logs: {} });
|
||||
useTools.setState({
|
||||
tools: [],
|
||||
loaded: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
jobs: {},
|
||||
logs: {},
|
||||
updates: {},
|
||||
checkingUpdates: false,
|
||||
updatesChecked: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => setToolsBackend(null));
|
||||
@@ -264,6 +274,22 @@ describe("LanguageServersPage", () => {
|
||||
expect(screen.getByLabelText("gopls install log")).toHaveTextContent("Installing gopls");
|
||||
});
|
||||
|
||||
it("offers an update when a newer version of an installed tool is out", async () => {
|
||||
const user = userEvent.setup();
|
||||
backend.available = [{ name: "rust-analyzer", current: "1.85.0", latest: "1.86.0" }];
|
||||
renderPage();
|
||||
await waitFor(() => expect(listItem("rust-analyzer")).toHaveTextContent("Update"));
|
||||
expect(listItem("gopls")).not.toHaveTextContent("Update");
|
||||
await user.click(listItem("rust-analyzer"));
|
||||
const details = screen.getByLabelText("rust-analyzer details");
|
||||
expect(details).toHaveTextContent("Version 1.86.0 is available.");
|
||||
await user.click(within(details).getByRole("button", { name: "Update rust-analyzer" }));
|
||||
await waitFor(() => expect(listItem("rust-analyzer")).not.toHaveTextContent("Update"));
|
||||
expect(
|
||||
within(screen.getByLabelText("rust-analyzer details")).getByRole("button", { name: "Reinstall rust-analyzer" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("explains why a tool cannot be installed", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* simulation in plain-browser development and tests (no network, ever).
|
||||
*/
|
||||
import { Channel } from "@tauri-apps/api/core";
|
||||
import type { InstallEvent, RequestId, ToolInfo } from "@/bindings";
|
||||
import type { InstallEvent, RequestId, ToolInfo, ToolUpdate } from "@/bindings";
|
||||
import { call, commands, IpcError, isTauri } from "@/platform/ipc";
|
||||
|
||||
export interface LanguageChoice {
|
||||
@@ -15,6 +15,8 @@ export interface ToolsBackend {
|
||||
list(): Promise<ToolInfo[]>;
|
||||
install(name: string, request: RequestId, onEvent: (e: InstallEvent) => void): Promise<ToolInfo>;
|
||||
uninstall(name: string): Promise<ToolInfo>;
|
||||
/** Newer versions of the tools the IDE installed (release downloads and npm packages). */
|
||||
updates(request: RequestId): Promise<ToolUpdate[]>;
|
||||
cancel(request: RequestId): void;
|
||||
/** Restarts the language servers named `name` in every open project (after an install). */
|
||||
restartServers(name: string): Promise<void>;
|
||||
@@ -32,6 +34,7 @@ export const tauriToolsBackend: ToolsBackend = {
|
||||
return call(commands.toolsInstall(name, request, channel));
|
||||
},
|
||||
uninstall: (name) => call(commands.toolsUninstall(name)),
|
||||
updates: (request) => call(commands.toolsUpdates(request)),
|
||||
cancel: (request) => void commands.cancelRequest(request).catch(() => {}),
|
||||
// Every open project: Settings belongs to one project window, but all of them run the tool.
|
||||
restartServers: async (name) => {
|
||||
@@ -150,6 +153,8 @@ export class MemoryToolsBackend implements ToolsBackend {
|
||||
/** Delay between simulated steps (ms). */
|
||||
stepMs: number;
|
||||
failNext: string | null = null;
|
||||
/** What `updates` reports, for the tools that are installed. */
|
||||
available: ToolUpdate[] = [];
|
||||
private cancelled = new Set<RequestId>();
|
||||
|
||||
constructor(tools: ToolInfo[] = sampleTools(), stepMs = 120) {
|
||||
@@ -161,6 +166,10 @@ export class MemoryToolsBackend implements ToolsBackend {
|
||||
return this.tools.map((t) => ({ ...t }));
|
||||
}
|
||||
|
||||
async updates() {
|
||||
return this.available.filter((u) => this.tools.some((t) => t.name === u.name && t.status === "installed"));
|
||||
}
|
||||
|
||||
private replace(next: ToolInfo) {
|
||||
this.tools = this.tools.map((t) => (t.name === next.name ? next : t));
|
||||
return { ...next };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Tool list + running installs. Installs show in the status-bar task list too. */
|
||||
import { create } from "zustand";
|
||||
import type { RequestId, ToolInfo } from "@/bindings";
|
||||
import type { RequestId, ToolInfo, ToolUpdate } from "@/bindings";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { isCancelled, nextRequestId } from "@/platform/ipc";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
@@ -28,6 +28,11 @@ interface ToolsState {
|
||||
jobs: Readonly<Record<string, InstallJob>>;
|
||||
/** Output of the last finished install per tool (kept for the log view). */
|
||||
logs: Readonly<Record<string, { lines: string[]; error: string | null }>>;
|
||||
/** Newer versions of the tools the IDE installed, from the last check. */
|
||||
updates: Readonly<Record<string, ToolUpdate>>;
|
||||
checkingUpdates: boolean;
|
||||
/** Whether updates were looked for in this session (the Language Servers page looks once). */
|
||||
updatesChecked: boolean;
|
||||
}
|
||||
|
||||
export const useTools = create<ToolsState>(() => ({
|
||||
@@ -37,6 +42,9 @@ export const useTools = create<ToolsState>(() => ({
|
||||
error: null,
|
||||
jobs: {},
|
||||
logs: {},
|
||||
updates: {},
|
||||
checkingUpdates: false,
|
||||
updatesChecked: false,
|
||||
}));
|
||||
|
||||
const errorText = (e: unknown) => (e instanceof Error ? e.message : String(e));
|
||||
@@ -59,6 +67,25 @@ export function refreshTools(): Promise<void> {
|
||||
return refreshing;
|
||||
}
|
||||
|
||||
let checkingUpdates: Promise<void> | null = null;
|
||||
|
||||
/** Looks for newer versions of the tools the IDE installed; offline, none are shown. */
|
||||
export function checkToolUpdates(): Promise<void> {
|
||||
checkingUpdates ??= (async () => {
|
||||
useTools.setState({ checkingUpdates: true });
|
||||
try {
|
||||
const found = await toolsBackend().updates(nextRequestId());
|
||||
useTools.setState({ updates: Object.fromEntries(found.map((u) => [u.name, u])) });
|
||||
} catch {
|
||||
// A failed lookup leaves the last result.
|
||||
} finally {
|
||||
useTools.setState({ checkingUpdates: false, updatesChecked: true });
|
||||
checkingUpdates = null;
|
||||
}
|
||||
})();
|
||||
return checkingUpdates;
|
||||
}
|
||||
|
||||
function upsertTool(info: ToolInfo) {
|
||||
useTools.setState((s) => {
|
||||
const i = s.tools.findIndex((t) => t.name === info.name);
|
||||
@@ -124,6 +151,10 @@ export function installTool(name: string, opts: { notificationKey?: string } = {
|
||||
});
|
||||
upsertTool(info);
|
||||
endJob(name, null);
|
||||
useTools.setState((s) => {
|
||||
const { [name]: _updated, ...updates } = s.updates;
|
||||
return { updates };
|
||||
});
|
||||
void toolsBackend()
|
||||
.restartServers(name)
|
||||
.catch(() => {});
|
||||
|
||||
@@ -4786,6 +4786,8 @@
|
||||
"{count} file": "{count} 个文件",
|
||||
"{count} files": "{count} 个文件",
|
||||
"Reinstall {name}": "重新安装 {name}",
|
||||
"Update {displayName}": "更新 {displayName}",
|
||||
"Version {latest} is available.": "有新版本 {latest} 可用。",
|
||||
"Installed in {path}; it is used for projects targeting Java {version}.": "已安装到 {path};用于以 Java {version} 为目标的项目。",
|
||||
"Code insight for {languages} needs {command}. Install it via {method}?": "{languages} 的代码洞察需要 {command}。是否通过 {method} 安装?",
|
||||
"Version {version}": "版本 {version}",
|
||||
|
||||
Reference in New Issue
Block a user