mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
fix(cpp): explain missing clang-tidy checks with Apple's clangd (#770)
Apple's clangd (/usr/bin/clangd, Xcode and CommandLineTools toolchains) is built without clang-tidy, so on a Mac without a managed clangd the checks from .clang-tidy never appear. When a clangd server for C, C++, Objective-C or Objective-C++ resolves to Apple's binary and the project has a .clang-tidy, show one warning per session with Install (managed LLVM clangd, which already takes precedence once installed) and Language Servers… actions. Fixes #692.
This commit is contained in:
@@ -82,6 +82,12 @@ pub enum IdeEvent {
|
||||
status: ServerStatus,
|
||||
message: Option<String>,
|
||||
},
|
||||
/// A running server lacks a capability; the UI offers the managed tool as a replacement.
|
||||
LanguageServerWarning {
|
||||
name: String,
|
||||
message: String,
|
||||
install_tool: String,
|
||||
},
|
||||
/// A stopped language server left the project's list (disabled in settings, or a
|
||||
/// fallback that a preferred server took over from again).
|
||||
LanguageServerRemoved {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
//! Apple clangd accepts `.clang-tidy` projects but ships without clang-tidy checks.
|
||||
|
||||
use crate::{LspManager, Server};
|
||||
use ide_core::IdeEvent;
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) const MESSAGE: &str = "Apple's clangd does not run clang-tidy, so checks from .clang-tidy are not shown. Install clangd (LLVM) to get them.";
|
||||
|
||||
fn is_apple_clangd(path: &Path) -> bool {
|
||||
if path == Path::new("/usr/bin/clangd") {
|
||||
return true;
|
||||
}
|
||||
if !path.ends_with("usr/bin/clangd") {
|
||||
return false;
|
||||
}
|
||||
path.starts_with("/Library/Developer/CommandLineTools")
|
||||
|| path.ancestors().any(|ancestor| {
|
||||
ancestor.file_name().is_some_and(|name| name == "Toolchains")
|
||||
&& ancestor.parent().is_some_and(|developer| {
|
||||
developer.ends_with("Contents/Developer")
|
||||
&& developer.parent().and_then(Path::parent).and_then(Path::file_name).is_some_and(|name| {
|
||||
let name = name.to_string_lossy();
|
||||
name.starts_with("Xcode") && name.ends_with(".app")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn has_config(server_root: &Path, project_root: &Path, file: &Path) -> bool {
|
||||
server_root.join(".clang-tidy").is_file()
|
||||
|| file
|
||||
.parent()
|
||||
.into_iter()
|
||||
.flat_map(Path::ancestors)
|
||||
.take_while(|dir| dir.starts_with(project_root))
|
||||
.any(|dir| dir.join(".clang-tidy").is_file())
|
||||
}
|
||||
|
||||
impl LspManager {
|
||||
pub(crate) async fn check_clang_tidy(&self, server: &Server) {
|
||||
if server.name != "clangd"
|
||||
|| !cfg!(target_os = "macos")
|
||||
|| self.shown_messages.lock().contains(&("clangd".into(), MESSAGE.into()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
let binary = server.resolved_binary.read().clone();
|
||||
let Some(binary) = binary else { return };
|
||||
let files: Vec<_> = self
|
||||
.docs
|
||||
.lock()
|
||||
.values()
|
||||
.filter(|doc| {
|
||||
matches!(doc.language.as_str(), "c" | "cpp" | "objective-c" | "objective-cpp")
|
||||
&& doc.servers.iter().any(|attached| attached.server.id == server.id)
|
||||
})
|
||||
.map(|doc| doc.path.clone())
|
||||
.collect();
|
||||
let root = server.root.clone();
|
||||
let Some(me) = self.me.upgrade() else { return };
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
if !is_apple_clangd(&binary) && !std::fs::canonicalize(&binary).is_ok_and(|path| is_apple_clangd(&path)) {
|
||||
return;
|
||||
}
|
||||
if !me.shutting_down.load(std::sync::atomic::Ordering::Acquire)
|
||||
&& files.iter().any(|file| has_config(&root, &me.project_root_for(file), file))
|
||||
&& me.first_message("clangd", MESSAGE)
|
||||
{
|
||||
me.sink.emit(IdeEvent::LanguageServerWarning {
|
||||
name: "clangd".into(),
|
||||
message: MESSAGE.into(),
|
||||
install_tool: "clangd".into(),
|
||||
});
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn apple_paths() {
|
||||
for path in [
|
||||
"/usr/bin/clangd",
|
||||
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clangd",
|
||||
"/Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clangd",
|
||||
"/Library/Developer/CommandLineTools/usr/bin/clangd",
|
||||
] {
|
||||
assert!(is_apple_clangd(Path::new(path)), "{path}");
|
||||
}
|
||||
for path in [
|
||||
"/Users/me/Library/Application Support/openidea/tools/clangd/bin/clangd",
|
||||
"/opt/homebrew/opt/llvm/bin/clangd",
|
||||
"/usr/local/bin/clangd",
|
||||
"/usr/bin/clangd-custom",
|
||||
"/Library/Developer/CommandLineTools-custom/usr/bin/clangd",
|
||||
"/Applications/Other.app/Contents/Developer/Toolchains/LLVM.xctoolchain/usr/bin/clangd",
|
||||
] {
|
||||
assert!(!is_apple_clangd(Path::new(path)), "{path}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_in_server_root_or_file_ancestors_stays_inside_project() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let project = dir.path().join("project");
|
||||
let server = project.join("build");
|
||||
let source = project.join("src/nested");
|
||||
let file = source.join("main.cpp");
|
||||
std::fs::create_dir_all(&source).unwrap();
|
||||
std::fs::create_dir_all(&server).unwrap();
|
||||
std::fs::write(dir.path().join(".clang-tidy"), "").unwrap();
|
||||
assert!(!has_config(&server, &project, &file));
|
||||
for root in [&server, &source, &project.join("src"), &project] {
|
||||
let config = root.join(".clang-tidy");
|
||||
std::fs::write(&config, "Checks: '-*,modernize-use-nullptr'").unwrap();
|
||||
assert!(has_config(&server, &project, &file), "{}", root.display());
|
||||
std::fs::remove_file(config).unwrap();
|
||||
}
|
||||
std::fs::create_dir(source.join(".clang-tidy")).unwrap();
|
||||
assert!(!has_config(&server, &project, &file));
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,18 @@ pub trait Launcher: Send + Sync + 'static {
|
||||
self.launch(config, root, handler, on_stderr)
|
||||
}
|
||||
|
||||
/// Starts a server and reports its executable when the launcher runs a local process.
|
||||
fn launch_resolved(
|
||||
&self,
|
||||
config: &LanguageServerConfig,
|
||||
root: &Path,
|
||||
handler: Arc<dyn Handler>,
|
||||
on_stderr: Box<dyn Fn(String) + Send + Sync>,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<(Peer, Option<PathBuf>), LaunchError> {
|
||||
self.launch_cancellable(config, root, handler, on_stderr, cancel).map(|peer| (peer, None))
|
||||
}
|
||||
|
||||
/// Whether `config` could start without a `Missing` error.
|
||||
fn available(&self, _config: &LanguageServerConfig, _root: &Path) -> bool {
|
||||
true
|
||||
@@ -72,6 +84,17 @@ impl Launcher for ProcessLauncher {
|
||||
on_stderr: Box<dyn Fn(String) + Send + Sync>,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<Peer, LaunchError> {
|
||||
self.launch_resolved(config, root, handler, on_stderr, cancel).map(|(peer, _)| peer)
|
||||
}
|
||||
|
||||
fn launch_resolved(
|
||||
&self,
|
||||
config: &LanguageServerConfig,
|
||||
root: &Path,
|
||||
handler: Arc<dyn Handler>,
|
||||
on_stderr: Box<dyn Fn(String) + Send + Sync>,
|
||||
cancel: &CancellationToken,
|
||||
) -> Result<(Peer, Option<PathBuf>), LaunchError> {
|
||||
let search = server_search_path(config);
|
||||
let binary = ide_core::tool_path::find_command(&config.command, &search, root)
|
||||
.and_then(|b| resolve_rustup_proxy(&config.command, b));
|
||||
@@ -87,10 +110,12 @@ impl Launcher for ProcessLauncher {
|
||||
handler,
|
||||
on_stderr,
|
||||
cancel,
|
||||
);
|
||||
)
|
||||
.map(|peer| (peer, None));
|
||||
}
|
||||
let binary = binary.ok_or(LaunchError::Missing)?;
|
||||
Peer::spawn(&binary.to_string_lossy(), &config.args, root, &env, handler, on_stderr)
|
||||
.map(|peer| (peer, Some(binary.clone())))
|
||||
.map_err(|e| LaunchError::Failed(format!("failed to start {}: {e}", binary.display())))
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
mod angular;
|
||||
mod caps;
|
||||
mod clang_tidy;
|
||||
mod convert;
|
||||
mod external_project;
|
||||
mod file_ops;
|
||||
@@ -472,13 +473,16 @@ impl LspManager {
|
||||
if !me.execution_allowed.load(Ordering::Acquire) || cancel.is_cancelled() {
|
||||
return Err(LaunchError::Config("Project is in Restricted Mode".into()));
|
||||
}
|
||||
me.launcher.launch_cancellable(&cfg, &root, handler, Box::new(move |line| tail.push_stderr(line)), &cancel)
|
||||
me.launcher.launch_resolved(&cfg, &root, handler, Box::new(move |line| tail.push_stderr(line)), &cancel)
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|e| Err(LaunchError::Failed(e.to_string())));
|
||||
|
||||
let peer = match launched {
|
||||
Ok(peer) => peer,
|
||||
Ok((peer, binary)) => {
|
||||
*server.resolved_binary.write() = binary;
|
||||
peer
|
||||
}
|
||||
Err(LaunchError::Missing) => {
|
||||
let cfg = server.effective_config();
|
||||
let message =
|
||||
@@ -587,6 +591,7 @@ impl LspManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.check_clang_tidy(&server).await;
|
||||
server.refresh_busy_status(&self.sink);
|
||||
server.announce_ready(generation);
|
||||
self.spawn_supervisor(server, generation, peer);
|
||||
@@ -810,6 +815,12 @@ impl LspManager {
|
||||
}
|
||||
for s in &servers {
|
||||
self.ensure_started(s);
|
||||
if s.running().is_some()
|
||||
&& let Some(me) = self.me.upgrade()
|
||||
{
|
||||
let server = s.clone();
|
||||
self.runtime.spawn(async move { me.check_clang_tidy(&server).await });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ pub struct Server {
|
||||
pub config: LanguageServerConfig,
|
||||
/// `config` with the settings hook applied, as the current instance was launched.
|
||||
effective: Mutex<Option<LanguageServerConfig>>,
|
||||
pub resolved_binary: RwLock<Option<PathBuf>>,
|
||||
languages: Mutex<Vec<LanguageId>>,
|
||||
state: Mutex<State>,
|
||||
/// Fired when the instance being started is abandoned (stop / restart while starting).
|
||||
@@ -114,6 +115,7 @@ impl Server {
|
||||
root,
|
||||
config,
|
||||
effective: Mutex::new(None),
|
||||
resolved_binary: RwLock::new(None),
|
||||
languages: Mutex::new(vec![language]),
|
||||
state: Mutex::new(State { generation: 0, conn: Conn::Idle }),
|
||||
start_cancel: Mutex::new(CancellationToken::new()),
|
||||
|
||||
@@ -21,6 +21,7 @@ use std::time::Duration;
|
||||
struct MockState {
|
||||
caps: Mutex<Value>,
|
||||
missing: std::sync::atomic::AtomicBool,
|
||||
binary: Mutex<Option<PathBuf>>,
|
||||
launches: AtomicUsize,
|
||||
notifications: Mutex<Vec<(String, Value)>>,
|
||||
requests: Mutex<Vec<(String, Value)>>,
|
||||
@@ -148,6 +149,18 @@ impl MockLauncher {
|
||||
}
|
||||
|
||||
impl Launcher for MockLauncher {
|
||||
fn launch_resolved(
|
||||
&self,
|
||||
config: &LanguageServerConfig,
|
||||
root: &Path,
|
||||
handler: Arc<dyn Handler>,
|
||||
on_stderr: Box<dyn Fn(String) + Send + Sync>,
|
||||
cancel: &ide_core::CancellationToken,
|
||||
) -> Result<(Peer, Option<PathBuf>), LaunchError> {
|
||||
self.launch_cancellable(config, root, handler, on_stderr, cancel)
|
||||
.map(|peer| (peer, self.state(&config.name).binary.lock().clone()))
|
||||
}
|
||||
|
||||
fn launch(
|
||||
&self,
|
||||
config: &LanguageServerConfig,
|
||||
@@ -2836,3 +2849,68 @@ async fn formatting_provider_selection_tracks_dynamic_registration() {
|
||||
.unwrap();
|
||||
assert!(!h.mgr.can_format(doc).await.unwrap());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn apple_clangd_warns_for_tidy_config_once_across_languages_and_restarts() {
|
||||
for language in ["c", "cpp", "objective-c", "objective-cpp"] {
|
||||
let h = harness_for(language, "src", &[("clangd", incremental_caps())], &[]);
|
||||
*h.launcher.state("clangd").binary.lock() = Some(PathBuf::from("/usr/bin/clangd"));
|
||||
std::fs::write(h.root.join(".clang-tidy"), "Checks: '-*,modernize-use-nullptr'").unwrap();
|
||||
h.open("main.src", "int *p = 0;");
|
||||
eventually(|| h.events.lock().iter().any(|e| matches!(e, IdeEvent::LanguageServerWarning { .. }))).await;
|
||||
let id = h.mgr.servers()[0].id;
|
||||
h.mgr.restart(id).await.unwrap();
|
||||
h.wait_status("clangd", ServerStatus::Ready).await;
|
||||
h.open("other.src", "int *p = 0;");
|
||||
h.mgr.shutdown_all().await;
|
||||
let events = h.events.lock();
|
||||
let warnings: Vec<_> = events
|
||||
.iter()
|
||||
.filter_map(|e| match e {
|
||||
IdeEvent::LanguageServerWarning { name, message, install_tool } => Some((name, message, install_tool)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(warnings.len(), 1, "{language}");
|
||||
assert_eq!(warnings[0].0, "clangd");
|
||||
assert!(warnings[0].1.contains(".clang-tidy"));
|
||||
assert_eq!(warnings[0].2, "clangd");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn clang_tidy_warning_requires_apple_binary_and_a_config() {
|
||||
for (binary, config) in [
|
||||
("/usr/bin/clangd", false),
|
||||
("/opt/homebrew/opt/llvm/bin/clangd", true),
|
||||
("/Users/me/Library/Application Support/openidea/tools/clangd/bin/clangd", true),
|
||||
] {
|
||||
let h = harness_for("cpp", "cpp", &[("clangd", incremental_caps())], &[]);
|
||||
*h.launcher.state("clangd").binary.lock() = Some(PathBuf::from(binary));
|
||||
if config {
|
||||
std::fs::write(h.root.join(".clang-tidy"), "Checks: '-*,modernize-use-nullptr'").unwrap();
|
||||
}
|
||||
h.open("main.cpp", "int *p = 0;");
|
||||
h.wait_status("clangd", ServerStatus::Ready).await;
|
||||
h.mgr.shutdown_all().await;
|
||||
assert!(!h.events.lock().iter().any(|e| matches!(e, IdeEvent::LanguageServerWarning { .. })), "{binary}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn clang_tidy_config_for_a_later_opened_file_warns_on_a_running_server() {
|
||||
let h = harness_for("cpp", "cpp", &[("clangd", incremental_caps())], &[]);
|
||||
*h.launcher.state("clangd").binary.lock() = Some(PathBuf::from("/usr/bin/clangd"));
|
||||
h.open("main.cpp", "int *p = 0;");
|
||||
h.wait_status("clangd", ServerStatus::Ready).await;
|
||||
assert!(!h.events.lock().iter().any(|e| matches!(e, IdeEvent::LanguageServerWarning { .. })));
|
||||
std::fs::create_dir(h.root.join("nested")).unwrap();
|
||||
std::fs::write(h.root.join("nested/.clang-tidy"), "Checks: '-*,modernize-use-nullptr'").unwrap();
|
||||
h.open("nested/other.cpp", "int *p = 0;");
|
||||
eventually(|| h.events.lock().iter().any(|e| matches!(e, IdeEvent::LanguageServerWarning { .. }))).await;
|
||||
assert_eq!(h.mgr.servers().len(), 1);
|
||||
h.mgr.shutdown_all().await;
|
||||
}
|
||||
|
||||
@@ -3619,6 +3619,8 @@ cancellable: boolean } |
|
||||
{ type: "indexState"; indexing: boolean; files: number; symbols: number } |
|
||||
/** Git status / branch information should be refreshed. */
|
||||
{ type: "vcsChanged"; root: IpcPath } | { type: "languageServer"; server: ServerId; name: string; status: ServerStatus; message: string | null } |
|
||||
/** A running server lacks a capability; the UI offers the managed tool as a replacement. */
|
||||
{ type: "languageServerWarning"; name: string; message: string; installTool: string } |
|
||||
/**
|
||||
* A stopped language server left the project's list (disabled in settings, or a
|
||||
* fallback that a preferred server took over from again).
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useTasks } from "@/workbench/tasks";
|
||||
import { MemoryToolsBackend, sampleTools, setToolsBackend } from "../backend";
|
||||
import { cancelInstall, installTool, refreshTools, uninstallTool, useTools } from "../store";
|
||||
import { filterTools, LanguageServersPage } from "../LanguageServersPage";
|
||||
import { installMissingServerPrompts, missingKey, promptInstall } from "../missing";
|
||||
import { installMissingServerPrompts, installServerWarningPrompts, missingKey, promptInstall } from "../missing";
|
||||
import { resetTestForm, stagedFor, TestSettingsForm } from "./settingsForm";
|
||||
|
||||
type Note = Parameters<NotificationService["notify"]>[0];
|
||||
@@ -273,3 +273,49 @@ describe("tools.install action", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("language server capability warning", () => {
|
||||
const event = {
|
||||
type: "languageServerWarning",
|
||||
name: "clangd",
|
||||
message:
|
||||
"Apple's clangd does not run clang-tidy, so checks from .clang-tidy are not shown. Install clangd (LLVM) to get them.",
|
||||
installTool: "clangd",
|
||||
} as const;
|
||||
|
||||
it("offers a managed replacement for system clangd once and restarts it after installation", async () => {
|
||||
backend.tools.push({
|
||||
...sampleTools()[0]!,
|
||||
name: "clangd",
|
||||
displayName: "clangd",
|
||||
command: "clangd",
|
||||
status: "onPath",
|
||||
path: "/usr/bin/clangd",
|
||||
languages: ["C", "C++", "Objective-C", "Objective-C++"],
|
||||
});
|
||||
const off = installServerWarningPrompts();
|
||||
try {
|
||||
emitLocalIdeEvent(event);
|
||||
emitLocalIdeEvent(event);
|
||||
await waitFor(() => expect(notes).toHaveLength(1));
|
||||
expect(notes[0]).toMatchObject({ title: "clangd", level: "warning", sticky: true, body: event.message });
|
||||
expect(notes[0]!.actions!.map((a) => a.label)).toEqual(["Install", "Language Servers…"]);
|
||||
notes[0]!.actions![0]!.run();
|
||||
await waitFor(() => expect(backend.restarted).toEqual(["clangd"]));
|
||||
expect(notes.at(-1)).toMatchObject({ title: "clangd installed", key: notes[0]!.key });
|
||||
} finally {
|
||||
off();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the Settings action when the tool cannot be installed", async () => {
|
||||
const off = installServerWarningPrompts();
|
||||
try {
|
||||
emitLocalIdeEvent(event);
|
||||
await waitFor(() => expect(notes).toHaveLength(1));
|
||||
expect(notes[0]!.actions!.map((a) => a.label)).toEqual(["Language Servers…"]);
|
||||
} finally {
|
||||
off();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { lazyComponent } from "@/ui";
|
||||
import { executeAction, registerActions } from "@/platform/actions";
|
||||
import { registerSettingsPage } from "@/workbench/contributions";
|
||||
import { installJdkPrompts, registerJdkActions } from "./jdk";
|
||||
import { installMissingServerPrompts, promptInstall } from "./missing";
|
||||
import { installMissingServerPrompts, installServerWarningPrompts, promptInstall } from "./missing";
|
||||
import { installTool, refreshTools, SETTINGS_PAGE_ID, toolByName, useTools } from "./store";
|
||||
|
||||
/** The Language Servers page (also used by the Settings window). */
|
||||
@@ -79,6 +79,7 @@ export function activate(): () => void {
|
||||
},
|
||||
]),
|
||||
installMissingServerPrompts(),
|
||||
installServerWarningPrompts(),
|
||||
registerJdkActions(),
|
||||
installJdkPrompts(),
|
||||
];
|
||||
|
||||
@@ -56,3 +56,31 @@ export function installMissingServerPrompts(): () => void {
|
||||
void promptInstall(e.name).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
/** A system server that lacks a capability can be replaced by the managed tool. */
|
||||
export function installServerWarningPrompts(): () => void {
|
||||
const prompted = new Set<string>();
|
||||
return onIdeEvent("languageServerWarning", (e) => {
|
||||
const key = `lsp-warning-${e.name}-${e.installTool}`;
|
||||
if (prompted.has(key)) return;
|
||||
prompted.add(key);
|
||||
void (async () => {
|
||||
if (!useTools.getState().loaded) await refreshTools();
|
||||
const tool = toolByName(e.installTool);
|
||||
notifications().notify({
|
||||
group: "lsp",
|
||||
level: "warning",
|
||||
title: e.name,
|
||||
body: tx(e.message),
|
||||
key,
|
||||
sticky: true,
|
||||
actions: [
|
||||
...(tool?.canInstall
|
||||
? [{ label: "Install", run: () => void installTool(e.installTool, { notificationKey: key }) }]
|
||||
: []),
|
||||
{ label: "Language Servers…", run: () => void executeAction("file.settings", SETTINGS_PAGE_ID) },
|
||||
],
|
||||
});
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"Apple's clangd does not run clang-tidy, so checks from .clang-tidy are not shown. Install clangd (LLVM) to get them.": "Apple 的 clangd 不运行 clang-tidy,因此不会显示 .clang-tidy 中配置的检查。请安装 clangd(LLVM)以启用这些检查。",
|
||||
"Newer dependency version available": "有更新的依赖版本可用",
|
||||
"Reports newer crate versions that Cargo.toml requirements do not already allow.": "报告 Cargo.toml 版本要求尚未允许的较新 crate 版本。",
|
||||
"Update to {latest}": "更新至 {latest}",
|
||||
|
||||
Reference in New Issue
Block a user