mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
feat(run): JAR Application run configurations and their import (#2409)
Fixes #2393 Importing run configurations from .run/ and .idea/ skipped every JarApplication entry because there was no equivalent type. Adds a JAR Application run configuration (path to JAR, VM options, program arguments, JRE, working directory, environment) that runs java [VM options] -jar <jar> [args] and debugs over JDWP like the Application type, and maps JarApplication on import (JAR_PATH, VM/program parameters, working directory, alternative JRE path, envs).
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
//! than one place is taken from the first, in that order.
|
||||
//!
|
||||
//! Python, Django server, Django tests, pytest, npm, Jest, Vitest, Node.js, Shell Script,
|
||||
//! Application, Spring Boot, Go Application / Go Test and compound configurations become the
|
||||
//! Application, JAR Application, Spring Boot, Go Application / Go Test and compound configurations become the
|
||||
//! matching kinds; other types are skipped with their type.
|
||||
//! `$PROJECT_DIR$` (and `$MODULE_DIR$`, `$MODULE_WORKING_DIR$`, `$ContentRoot$`,
|
||||
//! `$ProjectFileDir$`, all the project directory here) and `$USER_HOME$` are replaced; another
|
||||
@@ -164,6 +164,7 @@ const NODE: &str = "NodeJSConfigurationType";
|
||||
const SHELL: &str = "ShConfigurationType";
|
||||
const COMPOUND: &str = "CompoundRunConfigurationType";
|
||||
const APPLICATION: &str = "Application";
|
||||
const JAR_APPLICATION: &str = "JarApplication";
|
||||
const SPRING_BOOT: &str = "SpringBootApplicationConfigurationType";
|
||||
const GO_APPLICATION: &str = "GoApplicationRunConfiguration";
|
||||
const GO_TEST: &str = "GoTestRunConfiguration";
|
||||
@@ -344,6 +345,7 @@ impl Cx<'_> {
|
||||
NODE => self.node(name, c, &mut ignored)?,
|
||||
SHELL => self.shell(name, c)?,
|
||||
APPLICATION => self.java_app(name, c, false, &mut ignored)?,
|
||||
JAR_APPLICATION => self.jar_app(name, c, &mut ignored)?,
|
||||
SPRING_BOOT => self.java_app(name, c, true, &mut ignored)?,
|
||||
GO_APPLICATION => self.go_config(name, c, false, &mut ignored)?,
|
||||
GO_TEST => self.go_config(name, c, true, &mut ignored)?,
|
||||
@@ -793,6 +795,45 @@ impl Cx<'_> {
|
||||
|
||||
// ------------------------------------------------------------- Java
|
||||
|
||||
fn jar_app(&self, name: &str, c: &El, ignored: &mut Vec<String>) -> Res<RunConfig> {
|
||||
let Some(jar) = self.path(c.option("JAR_PATH"))? else { return Err(SkipReason::NothingToRun) };
|
||||
let (dir, cwd) = self.cwd(c.option("WORKING_DIRECTORY"))?;
|
||||
let jar = path_from(self.root, jar, &dir);
|
||||
let vm = self.words(c.option("VM_PARAMETERS"))?;
|
||||
let program = self.words(c.option("PROGRAM_PARAMETERS"))?;
|
||||
let mut params = BTreeMap::from([
|
||||
("jarPath".to_owned(), jar.clone()),
|
||||
("vmOptions".to_owned(), format_args(&vm)),
|
||||
("programArgs".to_owned(), format_args(&program)),
|
||||
]);
|
||||
let mut command = "java".to_owned();
|
||||
if c.flag("ALTERNATIVE_JRE_PATH_ENABLED")
|
||||
&& let Some(jre) = self.path(c.option("ALTERNATIVE_JRE_PATH"))?
|
||||
{
|
||||
// IDEA also stores SDK names here; those don't identify a JRE on this machine.
|
||||
if jre.contains('/') || jre.contains('\\') {
|
||||
let jre = jre.replace('\\', "/");
|
||||
command = if jre.ends_with("/bin/java") || jre.to_ascii_lowercase().ends_with("/bin/java.exe") {
|
||||
jre.clone()
|
||||
} else {
|
||||
format!("{}/bin/{}", jre.trim_end_matches('/'), if cfg!(windows) { "java.exe" } else { "java" })
|
||||
};
|
||||
params.insert("jre".to_owned(), jre);
|
||||
} else {
|
||||
ignored.push(format!("JRE {jre}"));
|
||||
}
|
||||
}
|
||||
let mut args = vm;
|
||||
args.extend(["-jar".to_owned(), jar]);
|
||||
args.extend(program);
|
||||
let mut rc = config(name, "jar-application", command, args, Some("java-debug"));
|
||||
rc.cwd = cwd.map(Into::into);
|
||||
rc.extras.params = params;
|
||||
rc.extras.include_system_env = c.option("PASS_PARENT_ENVS") != Some("false");
|
||||
self.envs(c, &mut rc);
|
||||
Ok(rc)
|
||||
}
|
||||
|
||||
/// IDEA's Application and Spring Boot types, as the `application` / `spring-boot` forms run
|
||||
/// them: `java <vm options> -cp ${classpath} <main> [--spring.profiles.active=…] <arguments>`
|
||||
/// with the module's runtime classpath.
|
||||
|
||||
@@ -60,6 +60,78 @@ fn cwd(c: &RunConfig) -> Option<PathBuf> {
|
||||
c.cwd.as_ref().map(|p| p.as_ref().to_path_buf())
|
||||
}
|
||||
|
||||
// JarApplicationConfiguration.writeExternal serializes its bean as options and its environment as envs:
|
||||
// https://github.com/JetBrains/intellij-community/blob/master/java/execution/impl/src/com/intellij/execution/jar/JarApplicationConfiguration.java
|
||||
const JAR: &str = r#"<configuration default="false" name="NAME" type="JarApplication">
|
||||
<option name="JAR_PATH" value="$PROJECT_DIR$/dist/server app.jar" />
|
||||
<option name="VM_PARAMETERS" value="-Xmx1g -Dname="My Server"" />
|
||||
<option name="PROGRAM_PARAMETERS" value="--config "$PROJECT_DIR$/server config.yml" """ />
|
||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/server" />
|
||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
|
||||
<option name="ALTERNATIVE_JRE_PATH" value="$PROJECT_DIR$/jdk" />
|
||||
<option name="PASS_PARENT_ENVS" value="false" />
|
||||
<envs><env name="MODE" value="production" /></envs>
|
||||
<method v="2" />
|
||||
</configuration>"#;
|
||||
|
||||
#[test]
|
||||
fn jar_applications_from_every_configuration_location() {
|
||||
for location in [".run/Server.run.xml", ".idea/runConfigurations/Server.xml", ".idea/workspace.xml"] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let xml = if location.ends_with("workspace.xml") {
|
||||
workspace(&JAR.replace("NAME", "Server"))
|
||||
} else {
|
||||
run_file("Server", JAR)
|
||||
};
|
||||
write(dir.path(), location, &xml);
|
||||
let r = import_from(dir.path(), &[]);
|
||||
let jar = get(&r, "Server");
|
||||
let root = dir.path().to_string_lossy().replace('\\', "/");
|
||||
assert_eq!(jar.kind, "jar-application");
|
||||
let java = if cfg!(windows) { "java.exe" } else { "java" };
|
||||
assert_eq!(jar.command, format!("{root}/jdk/bin/{java}"));
|
||||
assert_eq!(
|
||||
jar.args,
|
||||
s(&[
|
||||
"-Xmx1g",
|
||||
"-Dname=My Server",
|
||||
"-jar",
|
||||
"../dist/server app.jar",
|
||||
"--config",
|
||||
&format!("{root}/server config.yml"),
|
||||
""
|
||||
])
|
||||
);
|
||||
assert_eq!(cwd(jar), Some(PathBuf::from("server")));
|
||||
assert_eq!(jar.debug_adapter.as_deref(), Some("java-debug"));
|
||||
assert_eq!(jar.env["MODE"], "production");
|
||||
assert!(!jar.extras.include_system_env);
|
||||
assert_eq!(jar.extras.params["jarPath"], "../dist/server app.jar");
|
||||
assert_eq!(jar.extras.params["jre"], format!("{root}/jdk"));
|
||||
assert!(ignored(&r, "Server").is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jar_application_defaults_and_invalid_targets() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
for (name, xml) in [
|
||||
("Default", JAR.replace("value=\"true\"", "value=\"false\"")),
|
||||
("Named", JAR.replace("$PROJECT_DIR$/jdk", "temurin-21")),
|
||||
("NoJar", JAR.replace("$PROJECT_DIR$/dist/server app.jar", "")),
|
||||
("UnknownMacro", JAR.replace("$PROJECT_DIR$/dist", "$JAR_HOME$")),
|
||||
] {
|
||||
write(dir.path(), &format!(".run/{name}.run.xml"), &run_file(name, &xml));
|
||||
}
|
||||
let r = import_from(dir.path(), &[]);
|
||||
assert_eq!(get(&r, "Default").command, "java");
|
||||
assert!(!get(&r, "Default").extras.params.contains_key("jre"));
|
||||
assert_eq!(get(&r, "Named").command, "java");
|
||||
assert_eq!(ignored(&r, "Named"), ["JRE temurin-21"]);
|
||||
assert_eq!(reason(&r, "NoJar"), &SkipReason::NothingToRun);
|
||||
assert_eq!(reason(&r, "UnknownMacro"), &SkipReason::PathVariable { variable: "$JAR_HOME$".into() });
|
||||
}
|
||||
|
||||
/// A Django project in `src/` with a root virtualenv.
|
||||
fn django_project() -> tempfile::TempDir {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -22,6 +22,7 @@ const KIND_LABELS: Record<string, string> = {
|
||||
program: "Program",
|
||||
compound: "Compound",
|
||||
application: "Application",
|
||||
"jar-application": "JAR Application",
|
||||
"spring-boot": "Spring Boot",
|
||||
node: "Node.js",
|
||||
"java-file": "Java Single File",
|
||||
@@ -51,6 +52,7 @@ export const KIND_OPTIONS = Object.entries(KIND_LABELS).map(([value, label]) =>
|
||||
export const KIND_CATEGORIES: Record<string, string> = {
|
||||
"spring-boot": "Java",
|
||||
application: "Java",
|
||||
"jar-application": "Java",
|
||||
native: "Java",
|
||||
"java-file": "Java",
|
||||
kotlin: "Java",
|
||||
@@ -105,6 +107,7 @@ export function configIcon(config: Pick<RunConfig, "kind" | "isTest">): string {
|
||||
case "compound":
|
||||
return "gutter-run-all";
|
||||
case "program":
|
||||
case "jar-application":
|
||||
return "file-binary";
|
||||
case "spring-boot":
|
||||
return "spring-boot";
|
||||
|
||||
@@ -45,6 +45,7 @@ export function jdwpLaunch(config: RunConfig, jdwpPort?: number): RunConfig | nu
|
||||
config = applyBuildPolicyToCommand(config);
|
||||
if (config.kind === "tomcat") return { ...config, params: { ...config.params, tomcatDebug: "true" } };
|
||||
const args = config.args;
|
||||
if (config.kind === "jar-application") return { ...config, args: [JDWP_AGENT, ...args] };
|
||||
// A single source file (`java Hi.java`): the agent goes before the file. Compiled first
|
||||
// (`javac … && java -cp …`): through `JDK_JAVA_OPTIONS`, which only the `java` launcher reads.
|
||||
if (config.kind === "java-file")
|
||||
|
||||
@@ -17,10 +17,11 @@ vi.mock("@/platform/ipc", async (orig) => {
|
||||
});
|
||||
|
||||
const { usePlatform } = await import("@/platform/platformInfo");
|
||||
const { parseJavaArgs, joinJavaArgs } = await import("@/platform/javaLaunch");
|
||||
const { parseJavaArgs, joinJavaArgs, isJvmApplication, JDWP_AGENT } = await import("@/platform/javaLaunch");
|
||||
const { parseArgs } = await import("./configs");
|
||||
const { jdwpLaunch } = await import("./javaDebug");
|
||||
const { applicationType, gradleJvmArgs, joinPropertyArgs, springBootType } = await import("./jvmTypes");
|
||||
const { applicationType, jarApplicationType, gradleJvmArgs, joinPropertyArgs, springBootType } =
|
||||
await import("./jvmTypes");
|
||||
const { formParams, newConfigOfType, registerBuiltinTypes, typeFor, withParams } = await import("./types");
|
||||
|
||||
const REPORTER_VM = "-Ddemo.flag=on --add-opens java.base/java.lang=ALL-UNNAMED";
|
||||
@@ -46,6 +47,63 @@ beforeEach(() => {
|
||||
});
|
||||
afterEach(() => dispose());
|
||||
|
||||
describe("JAR Application", () => {
|
||||
it("creates, edits and recovers a JAR configuration without a source module", () => {
|
||||
const created = newConfigOfType(jarApplicationType, []);
|
||||
expect(created.debugAdapter).toBe("java-debug");
|
||||
const edited = withParams(created, {
|
||||
jarPath: "dist/my app.jar",
|
||||
vmOptions: '-Xmx1g --add-opens java.base/java.lang=ALL-UNNAMED -Dname="My App"',
|
||||
programArgs: '--config "a b.yml" "" -jar other.jar',
|
||||
jre: "/jdk 21",
|
||||
});
|
||||
expect(edited.command).toBe("/jdk 21/bin/java");
|
||||
expect(edited.args).toEqual([
|
||||
"-Xmx1g",
|
||||
"--add-opens",
|
||||
"java.base/java.lang=ALL-UNNAMED",
|
||||
"-Dname=My App",
|
||||
"-jar",
|
||||
"dist/my app.jar",
|
||||
"--config",
|
||||
"a b.yml",
|
||||
"",
|
||||
"-jar",
|
||||
"other.jar",
|
||||
]);
|
||||
const recovered = formParams({ ...edited, params: undefined });
|
||||
expect(recovered?.jarPath).toBe("dist/my app.jar");
|
||||
expect(jarApplicationType.commandLine(recovered!)).toEqual({ command: edited.command, args: edited.args });
|
||||
expect(parseJavaArgs(edited.args).mainClass).toBeNull();
|
||||
expect(isJvmApplication(edited)).toBe(false);
|
||||
const debug = jdwpLaunch(edited)!;
|
||||
expect(debug.args).toEqual([JDWP_AGENT, ...edited.args]);
|
||||
expect(debug.command).toBe(edited.command);
|
||||
});
|
||||
|
||||
it("uses java by default and keeps Windows JRE and JAR paths", () => {
|
||||
expect(jarApplicationType.commandLine({ jarPath: "app.jar" })).toEqual({
|
||||
command: "java",
|
||||
args: ["-jar", "app.jar"],
|
||||
});
|
||||
usePlatform.setState({ os: "windows" });
|
||||
try {
|
||||
expect(
|
||||
jarApplicationType.commandLine({
|
||||
jre: String.raw`C:\Program Files\jdk`,
|
||||
jarPath: String.raw`C:\apps\my app.jar`,
|
||||
programArgs: String.raw`"C:\data files\x"`,
|
||||
}),
|
||||
).toEqual({
|
||||
command: "C:\\Program Files\\jdk/bin/java.exe",
|
||||
args: ["-jar", String.raw`C:\apps\my app.jar`, String.raw`C:\data files\x`],
|
||||
});
|
||||
} finally {
|
||||
usePlatform.setState({ os: "macos" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Application / Spring Boot command line", () => {
|
||||
it.each([
|
||||
["application", applicationType],
|
||||
|
||||
@@ -175,6 +175,37 @@ function jvmApplicationType(spring: boolean): RunConfigTypeContribution {
|
||||
export const applicationType = jvmApplicationType(false);
|
||||
export const springBootType = jvmApplicationType(true);
|
||||
|
||||
export const jarApplicationType: RunConfigTypeContribution = {
|
||||
id: "jar-application",
|
||||
label: "JAR Application",
|
||||
icon: "file-binary",
|
||||
description: "Run an executable JAR",
|
||||
debugAdapter: "java-debug",
|
||||
fields: [
|
||||
{ key: "jarPath", label: "Path to JAR", mono: true, required: true, browse: "file" },
|
||||
vmOptionsField(),
|
||||
{ key: "programArgs", label: "Program arguments", mono: true },
|
||||
{ ...jreField, options: [{ value: "", label: "Default (java on PATH)" }] },
|
||||
],
|
||||
commandLine(p) {
|
||||
return {
|
||||
command: p.jre ? jreJava(p.jre) : p.executable || "java",
|
||||
args: [...splitJvmArgs(p.vmOptions), "-jar", p.jarPath?.trim() ?? "", ...splitJvmArgs(p.programArgs)],
|
||||
};
|
||||
},
|
||||
paramsFrom(c) {
|
||||
if (!/^java$/i.test(commandName(c.command))) return null;
|
||||
const parsed = parseJavaArgs(c.args);
|
||||
if (parsed.jarPath === undefined) return null;
|
||||
return {
|
||||
...(c.command !== "java" ? { executable: c.command } : {}),
|
||||
jarPath: parsed.jarPath,
|
||||
vmOptions: formatArgs(parsed.vmArgs),
|
||||
programArgs: formatArgs(parsed.programArgs),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* One value for a Maven property holding JVM arguments (`spring-boot.run.jvmArguments`,
|
||||
* `argLine`): the plugins split it on whitespace, honouring "…" and '…'.
|
||||
|
||||
@@ -91,6 +91,31 @@ it.each([undefined, false])(
|
||||
);
|
||||
|
||||
describe("configuration types", () => {
|
||||
it("shows the JAR fields, validates the path and edits shared working directory and environment", () => {
|
||||
const type = typeFor("jar-application")!;
|
||||
const config = newConfigOfType(type, []);
|
||||
expect(missingParams(config)).toEqual({ jarPath: "Path to JAR is required" });
|
||||
const edited = withParams(config, { jarPath: "app.jar", vmOptions: "-Xmx1g", programArgs: "hello" });
|
||||
const change = vi.fn();
|
||||
render(
|
||||
<ConfigForm
|
||||
config={{ ...edited, cwd: "server", env: { MODE: "test" } }}
|
||||
all={[]}
|
||||
type={type}
|
||||
errors={{}}
|
||||
onChange={change}
|
||||
modifyRequest={0}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByLabelText("Path to JAR")).toHaveValue("app.jar");
|
||||
expect(screen.getByLabelText("VM options")).toHaveValue("-Xmx1g");
|
||||
expect(screen.getByLabelText("Program arguments")).toHaveValue("hello");
|
||||
expect(screen.getByLabelText("Working directory")).toHaveValue("server");
|
||||
expect(screen.getByLabelText("Environment variables")).toHaveValue("MODE=test");
|
||||
fireEvent.change(screen.getByLabelText("Path to JAR"), { target: { value: "new.jar" } });
|
||||
expect(change.mock.calls.at(-1)?.[0]).toMatchObject({ args: ["-Xmx1g", "-jar", "new.jar", "hello"] });
|
||||
});
|
||||
|
||||
it("runs an Application with the chosen JRE and the classpath of the chosen module", () => {
|
||||
const type = typeFor("application")!;
|
||||
const base = withParams(newConfigOfType(type, []), { ...newConfigOfType(type, []).params, mainClass: "a.Main" });
|
||||
|
||||
@@ -403,6 +403,7 @@ describe("add service categories", () => {
|
||||
program: "Shell",
|
||||
compound: "Others",
|
||||
application: "Java",
|
||||
"jar-application": "Java",
|
||||
"spring-boot": "Java",
|
||||
native: "Java",
|
||||
"java-file": "Java",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { formatArgs, parseArgs, uid, uniqueName } from "./configs";
|
||||
import { commandName } from "./commandName";
|
||||
import {
|
||||
applicationType,
|
||||
jarApplicationType,
|
||||
gradleForkedJvm,
|
||||
isWindows,
|
||||
mavenForkedJvm,
|
||||
@@ -447,6 +448,7 @@ export const BUILTIN_TYPES: readonly RunConfigTypeContribution[] = [
|
||||
goBuildType,
|
||||
goTestType,
|
||||
applicationType,
|
||||
jarApplicationType,
|
||||
springBootType,
|
||||
tomcatType,
|
||||
compound,
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface JavaCommandLine {
|
||||
/** VM options, without the classpath. */
|
||||
vmArgs: string[];
|
||||
mainClass: string | null;
|
||||
jarPath?: string;
|
||||
programArgs: string[];
|
||||
/** Which classpath placeholder the configuration uses (`null`: an explicit classpath or none). */
|
||||
classpath: "runtime" | "test" | null;
|
||||
@@ -60,6 +61,15 @@ export function parseJavaArgs(args: readonly string[]): JavaCommandLine {
|
||||
let explicitClasspath: string | null = null;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i]!;
|
||||
if (a === "-jar")
|
||||
return {
|
||||
vmArgs,
|
||||
mainClass: null,
|
||||
jarPath: args[i + 1],
|
||||
programArgs: args.slice(i + 2),
|
||||
classpath,
|
||||
explicitClasspath,
|
||||
};
|
||||
if (a === "-cp" || a === "-classpath" || a === "--class-path") {
|
||||
const value = args[++i] ?? "";
|
||||
if (value === CLASSPATH) classpath = "runtime";
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"JAR Application": "JAR Application",
|
||||
"Run an executable JAR": "Run an executable JAR",
|
||||
"Path to JAR": "Path to JAR",
|
||||
"Default (java on PATH)": "Default (java on PATH)",
|
||||
"Stream Sample TUI": "Stream Sample TUI",
|
||||
"Arduino tool '{configured}' was not found. Set its path in Settings > Languages & Frameworks > Language Servers, or in settings.toml under languages.arduino.": "Arduino tool '{configured}' was not found. Set its path in Settings > Languages & Frameworks > Language Servers, or in settings.toml under languages.arduino.",
|
||||
"Arduino tool '{name}' was not found on PATH or in Arduino IDE. Install Arduino IDE 2, or set its path in Settings > Languages & Frameworks > Language Servers or settings.toml under languages.arduino.": "Arduino tool '{name}' was not found on PATH or in Arduino IDE. Install Arduino IDE 2, or set its path in Settings > Languages & Frameworks > Language Servers or settings.toml under languages.arduino.",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"JAR Application": "JAR 应用程序",
|
||||
"Run an executable JAR": "运行可执行 JAR",
|
||||
"Path to JAR": "JAR 路径",
|
||||
"Default (java on PATH)": "默认(PATH 中的 java)",
|
||||
"Arduino tool '{configured}' was not found. Set its path in Settings > Languages & Frameworks > Language Servers, or in settings.toml under languages.arduino.": "未找到 Arduino 工具“{configured}”。请在设置 > 语言和框架 > 语言服务器中配置其路径,或在 settings.toml 的 languages.arduino 下配置。",
|
||||
"Arduino tool '{name}' was not found on PATH or in Arduino IDE. Install Arduino IDE 2, or set its path in Settings > Languages & Frameworks > Language Servers or settings.toml under languages.arduino.": "在 PATH 或 Arduino IDE 中未找到 Arduino 工具“{name}”。请安装 Arduino IDE 2,或在设置 > 语言和框架 > 语言服务器中配置其路径,也可在 settings.toml 的 languages.arduino 下配置。",
|
||||
"Install Arduino IDE 2 (includes Arduino Language Server, Arduino CLI and clangd) and the board's core. Configure the server command and arguments in Settings > Languages & Frameworks > Language Servers, or set languages.arduino.cli, languages.arduino.clangd, languages.arduino.languageServer and languages.arduino.cliConfig in settings.toml. Set the board FQBN in sketch.yaml or languages.arduino.fqbn in settings.toml.": "请安装 Arduino IDE 2(包含 Arduino Language Server、Arduino CLI 和 clangd)及开发板核心。在设置 > 语言和框架 > 语言服务器中配置服务器命令和参数,或在 settings.toml 中设置 languages.arduino.cli、languages.arduino.clangd、languages.arduino.languageServer 和 languages.arduino.cliConfig。在 sketch.yaml 或 settings.toml 的 languages.arduino.fqbn 中设置开发板 FQBN。",
|
||||
|
||||
Reference in New Issue
Block a user