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

...
1 Коміти
Автор SHA1 Повідомлення Дата
l0ng-ai d0037c8440 feat(typescript): run TypeScript 7's own language server when it is the selected TypeScript
TypeScript 7 (typescript 7+, @typescript/native-preview) has no tsserver or
JavaScript API; its platform package's native executable serves LSP itself.
When Settings › Languages & Frameworks › TypeScript selects one (the
project's node_modules/typescript or a chosen directory), vtsls /
typescript-language-server is replaced by that executable with --lsp --stdio.
The project's TypeScript 7 is listed there as "TypeScript 7.x (native)".
2026-11-14 04:39:32 +08:00
4 змінених файлів з 115 додано та 3 видалено

No files matched your search

+21 -1
Переглянути файл
@@ -3,7 +3,7 @@
use crate::discover::{project_venv_python, workspace_tsdk};
use crate::exec::{path_with, search_path, which};
use crate::npm::{has_ts_api, own_typescript};
use crate::npm::{has_ts_api, native_typescript, own_typescript};
use crate::{angular, astro, godot, pinned, vue};
use crate::{bin_dir, exe, venv_of};
use ide_core::settings::{InspectionLevel, InspectionSettings, LanguageServerSettings, Settings};
@@ -722,6 +722,16 @@ pub fn tsdk(root: &Path, s: &Settings) -> Option<PathBuf> {
}
}
/// TypeScript 7's native executable when the selected TypeScript is one (see
/// [`native_typescript`]); `None` for the bundled one and TypeScript 6 and older.
pub fn native_tsdk(root: &Path, s: &Settings) -> Option<PathBuf> {
match s.languages.typescript.version.trim() {
"" | "bundled" => None,
"workspace" => native_typescript(&root.join("node_modules/typescript")),
custom => native_typescript(&setting_path(root, custom)?),
}
}
const PYTHON_SERVERS: [&str; 7] = ["basedpyright", "pyright", "ty", "pyrefly", "pylsp", "jedi-language-server", "ruff"];
pub(crate) fn merge(base: &mut Value, layer: Value) {
@@ -1140,6 +1150,16 @@ pub fn configure_server(cfg: &mut LanguageServerConfig, root: &Path, server_root
_ => {}
}
}
// TypeScript 7 serves LSP itself: it replaces the tsserver-based server.
if matches!(name, "vtsls" | "typescript-language-server")
&& let Some(native) = native_tsdk(server_root, s).or_else(|| native_tsdk(root, s))
{
cfg.command = native.to_string_lossy().into_owned();
cfg.args = vec!["--lsp".into(), "--stdio".into()];
cfg.initialization_options = None;
cfg.tsserver_project = false;
return;
}
let vue_command = s.tool_path(vue::SERVER).and_then(|p| setting_path(root, p));
let vue_command = vue_command.as_deref().map_or(vue::SERVER.into(), Path::to_string_lossy);
if name == "vtsls"
+14 -2
Переглянути файл
@@ -749,14 +749,26 @@ pub(crate) fn workspace_tsdk(root: &Path) -> Option<PathBuf> {
fn typescript(root: Option<&Path>) -> Vec<Sdk> {
let mut out = Vec::new();
if let Some(lib) = root.and_then(workspace_tsdk) {
// TypeScript 7 has no `tsserver.js`; its package directory stands for it (`native_tsdk`).
let native = || {
let package = root?.join("node_modules/typescript");
crate::npm::native_typescript(&package).map(|_| package.join("lib"))
};
if let Some(lib) = root.and_then(workspace_tsdk).or_else(native) {
let version = std::fs::read_to_string(lib.parent().unwrap_or(&lib).join("package.json"))
.ok()
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok())
.and_then(|v| v.get("version").and_then(|v| v.as_str()).map(str::to_owned));
out.push(Sdk {
path: lib.clone().into(),
name: format!("TypeScript {}", version.clone().unwrap_or_default()).trim().to_owned(),
name: format!(
"TypeScript {}{}",
version.clone().unwrap_or_default(),
if crate::npm::has_ts_api(&lib) { "" } else { " (native)" }
)
.replace(" ", " ")
.trim()
.to_owned(),
version,
source: "workspace".into(),
home: lib.parent().map(Into::into),
+35
Переглянути файл
@@ -10,6 +10,41 @@ pub(crate) fn has_ts_api(lib: &Path) -> bool {
lib.join("typescript.js").is_file()
}
/// The native executable of TypeScript 7 (`typescript` 7+, whose `tsc` is native, or
/// `@typescript/native-preview`'s `tsgo`), from its package directory or its `lib`.
/// These packages have no JavaScript API (`lib/typescript.js`); their platform package
/// (`@typescript/typescript-darwin-arm64`) holds the executable, which serves LSP itself
/// (`--lsp --stdio`).
pub(crate) fn native_typescript(dir: &Path) -> Option<PathBuf> {
let package = if dir.join("package.json").is_file() { dir } else { dir.parent()? };
if has_ts_api(&package.join("lib")) {
return None;
}
let manifest: serde_json::Value =
serde_json::from_slice(&std::fs::read(package.join("package.json")).ok()?).ok()?;
let (base, bin) = match manifest.get("name")?.as_str()? {
"typescript" => ("typescript", "tsc"),
"@typescript/native-preview" => ("native-preview", "tsgo"),
_ => return None,
};
let os = match std::env::consts::OS {
"macos" => "darwin",
"windows" => "win32",
os => os,
};
let arch = match std::env::consts::ARCH {
"aarch64" => "arm64",
"x86_64" => "x64",
arch => arch,
};
let platform = format!("{base}-{os}-{arch}");
// Next to the package (`node_modules/@typescript/…`) or nested in it.
package
.ancestors()
.map(|a| a.join("node_modules/@typescript").join(&platform).join("lib").join(crate::exe(bin)))
.find(|exe| exe.is_file())
}
/// The usable TypeScript library Node resolves from an installed server package.
pub(crate) fn own_typescript(package: &Path) -> Option<PathBuf> {
package.ancestors().map(|a| a.join("node_modules/typescript/lib")).find(|l| has_ts_api(l))
+45
Переглянути файл
@@ -1359,6 +1359,51 @@ fn a_server_chosen_from_a_node_installation_runs_on_that_node() {
assert_eq!(std::env::split_paths(&cfg.env["PATH"]).next(), Some(v20));
}
/// TypeScript 7 (`typescript` 7+, `@typescript/native-preview`) has no tsserver: choosing it
/// runs its native executable's own language server instead of vtsls / typescript-language-server.
#[test]
fn typescript_7_runs_its_own_language_server() {
let tmp = tempfile::tempdir().unwrap();
let modules = tmp.path().join("node_modules");
let os = match std::env::consts::OS {
"macos" => "darwin",
"windows" => "win32",
os => os,
};
let arch = match std::env::consts::ARCH {
"aarch64" => "arm64",
"x86_64" => "x64",
arch => arch,
};
std::fs::create_dir_all(modules.join("typescript/lib")).unwrap();
std::fs::write(modules.join("typescript/package.json"), r#"{"name":"typescript","version":"7.0.2"}"#).unwrap();
let native = modules.join(format!("@typescript/typescript-{os}-{arch}/lib")).join(exe("tsc"));
std::fs::create_dir_all(native.parent().unwrap()).unwrap();
std::fs::write(&native, "").unwrap();
let mut s = Settings::default();
s.languages.typescript.version = "workspace".into();
for name in ["vtsls", "typescript-language-server"] {
let mut cfg = server(name);
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
assert_eq!(Path::new(&cfg.command), native, "{name}");
assert_eq!(cfg.args, ["--lsp", "--stdio"]);
assert!(cfg.initialization_options.is_none());
}
// Picked as a directory (its `lib`, as for older versions), too.
s.languages.typescript.version = modules.join("typescript/lib").to_string_lossy().into_owned();
let mut cfg = server("vtsls");
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
assert_eq!(Path::new(&cfg.command), native);
// TypeScript 6 keeps the tsserver-based server, pointed at it.
std::fs::write(modules.join("typescript/lib/typescript.js"), "").unwrap();
std::fs::write(modules.join("typescript/lib/tsserver.js"), "").unwrap();
s.languages.typescript.version = "workspace".into();
let mut cfg = server("vtsls");
configure_server(&mut cfg, tmp.path(), tmp.path(), &s);
assert_eq!(cfg.command, "vtsls");
}
#[test]
fn the_godot_setting_applies_to_server_debugger_and_run_configurations() {
let tmp = tempfile::tempdir().unwrap();