дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(lsp): offer auto-imports from unopened files in projects without a tsconfig (#496)
Without a tsconfig.json or jsconfig.json, tsserver's inferred project holds only open files and what they import, so completion never offered exports from other files. Register the project's sources with tsserver as an external project, using the options an inferred project assumes, so no file is written to disk. Registration needs `typescript.tsserverRequest`. Projects with a configuration, including one in a subfolder, stay as they were, and so do trees over 10,000 sources or 16 MiB of JavaScript, since tsserver disables its language service past 20 MiB. Created and deleted files update the project, and unopened files stay out of Problems.
Цей коміт міститься в:
1 джерело
a3cd99db1f
коміт
8fabf054ef
16 files changed
+393
-1
No files matched your search
Створено
@@ -4372,6 +4372,7 @@ dependencies = [
|
||||
"ide-jsonrpc",
|
||||
"ide-language",
|
||||
"ide-text",
|
||||
"ignore",
|
||||
"lsp-types",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
|
||||
@@ -223,6 +223,7 @@ impl Host {
|
||||
let history = history.clone();
|
||||
let remote = self.remote.clone();
|
||||
let runtime = self.runtime.clone();
|
||||
let lsp = lsp.clone();
|
||||
Watcher::start(
|
||||
&root.clone(),
|
||||
excludes.clone(),
|
||||
@@ -230,6 +231,7 @@ impl Host {
|
||||
Box::new(move |batch| {
|
||||
let changed: Vec<PathBuf> = batch.created.iter().chain(&batch.modified).cloned().collect();
|
||||
index.update(&changed, &batch.removed);
|
||||
lsp.project_files_changed(batch.created.iter().chain(&batch.removed));
|
||||
// Atomic saves by external tools surface as `created`.
|
||||
documents.on_disk_changed(&changed);
|
||||
// A framework's root marker (`angular.json`) created or deleted: its
|
||||
@@ -358,6 +360,12 @@ impl Host {
|
||||
weak.upgrade().map(|p| p.reference_candidates(q, cancel)).unwrap_or_default()
|
||||
}));
|
||||
}
|
||||
{
|
||||
let weak = Arc::downgrade(&project);
|
||||
project.lsp.set_project_excludes_hook(Box::new(move || {
|
||||
weak.upgrade().map(|p| p.exclude_globs()).unwrap_or_default()
|
||||
}));
|
||||
}
|
||||
self.insert_project(window, project.clone());
|
||||
if let Some(cmd) = post_start {
|
||||
self.environment.spawn_post_start(cmd, project.root.clone(), project.sink.clone());
|
||||
|
||||
@@ -120,6 +120,9 @@ pub struct LanguageServerConfig {
|
||||
/// a JDK 21+); reported as the failure instead of launching it.
|
||||
#[serde(skip)]
|
||||
pub launch_error: Option<String>,
|
||||
/// Register source files with tsserver when no project configuration exists.
|
||||
#[serde(default)]
|
||||
pub tsserver_project: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -19,6 +19,7 @@ parking_lot.workspace = true
|
||||
tracing.workspace = true
|
||||
futures.workspace = true
|
||||
globset.workspace = true
|
||||
ignore.workspace = true
|
||||
toml.workspace = true
|
||||
percent-encoding = "2"
|
||||
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
//! Give tsserver a project for unopened sources, without creating a config file.
|
||||
use crate::LspManager;
|
||||
use crate::server::{Running, Server};
|
||||
use serde_json::{Value, json};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
pub type ExcludesFn = dyn Fn() -> Vec<String> + Send + Sync;
|
||||
const COMMAND: &str = "typescript.tsserverRequest";
|
||||
const LIMIT: usize = 10_000;
|
||||
const SCAN_LIMIT: usize = 100_000;
|
||||
/// tsserver turns its language service off past 20 MiB of JavaScript in a project.
|
||||
const SCRIPT_BYTES_LIMIT: u64 = 16 << 20;
|
||||
const REFRESH_DELAY: Duration = Duration::from_millis(300);
|
||||
const TIMEOUT: Duration = Duration::from_secs(15);
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct State {
|
||||
generation: u64,
|
||||
files: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
fn supported(server: &Server, run: &Running) -> bool {
|
||||
server.config.tsserver_project
|
||||
&& run
|
||||
.caps
|
||||
.read()
|
||||
.raw
|
||||
.pointer("/executeCommandProvider/commands")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|commands| commands.iter().any(|c| c.as_str() == Some(COMMAND)))
|
||||
}
|
||||
|
||||
fn configured(dir: &Path) -> bool {
|
||||
["tsconfig.json", "jsconfig.json"].iter().any(|name| dir.join(name).is_file())
|
||||
}
|
||||
|
||||
fn is_script(ext: &str) -> bool {
|
||||
matches!(ext, "js" | "jsx" | "mjs" | "cjs")
|
||||
}
|
||||
|
||||
fn is_source(ext: &str) -> bool {
|
||||
is_script(ext) || matches!(ext, "ts" | "tsx" | "mts" | "cts")
|
||||
}
|
||||
|
||||
/// All-or-nothing at the caps: a partial project could displace an inferred
|
||||
/// project while omitting some of its files. Configured trees stay with tsserver.
|
||||
fn source_files(root: &Path, exclude_root: &Path, excludes: Vec<String>, limit: usize) -> Vec<PathBuf> {
|
||||
if root.ancestors().any(configured) {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut ignores = ignore::gitignore::GitignoreBuilder::new(exclude_root);
|
||||
for pattern in excludes {
|
||||
if ignores.add_line(None, &pattern).is_err() {
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
let Ok(ignores) = ignores.build() else { return Vec::new() };
|
||||
let mut walk = ignore::WalkBuilder::new(root);
|
||||
walk.require_git(false).follow_links(false).filter_entry(move |entry| {
|
||||
if entry.depth() == 0 {
|
||||
return true;
|
||||
}
|
||||
let path = entry.path();
|
||||
let name = entry.file_name().to_string_lossy();
|
||||
!matches!(name.as_ref(), "node_modules" | "dist" | "build" | "out" | "coverage" | "target" | ".git")
|
||||
&& !ignores.matched_path_or_any_parents(path, entry.file_type().is_some_and(|t| t.is_dir())).is_ignore()
|
||||
});
|
||||
let mut files = Vec::new();
|
||||
let mut script_bytes = 0;
|
||||
for (visited, entry) in walk.build().enumerate() {
|
||||
// Do not claim a tree is config-less if we could not inspect it fully.
|
||||
let Ok(entry) = entry else { return Vec::new() };
|
||||
if visited >= SCAN_LIMIT {
|
||||
return Vec::new();
|
||||
}
|
||||
if entry.file_type().is_some_and(|t| t.is_dir()) && configured(entry.path()) {
|
||||
return Vec::new();
|
||||
}
|
||||
let ext = entry.path().extension().and_then(|s| s.to_str()).unwrap_or_default();
|
||||
if entry.file_type().is_some_and(|t| t.is_file()) && is_source(ext) {
|
||||
if is_script(ext) {
|
||||
script_bytes += entry.metadata().map_or(0, |m| m.len());
|
||||
if script_bytes > SCRIPT_BYTES_LIMIT {
|
||||
tracing::debug!("external TypeScript project exceeds its script size; retaining inferred projects");
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
files.push(entry.into_path());
|
||||
if files.len() > limit {
|
||||
tracing::debug!("external TypeScript project exceeds {limit} files; retaining inferred projects");
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
files
|
||||
}
|
||||
|
||||
fn project_name(server: &Server) -> PathBuf {
|
||||
server.root.join(".openidea-external-project")
|
||||
}
|
||||
|
||||
async fn request(run: &Running, command: &str, args: Value) -> bool {
|
||||
let request = run.peer.request::<_, Value>(
|
||||
"workspace/executeCommand",
|
||||
json!({
|
||||
"command": COMMAND,
|
||||
"arguments": [command, args, { "isAsync": false, "expectsResult": true }]
|
||||
}),
|
||||
None,
|
||||
);
|
||||
match tokio::time::timeout(TIMEOUT, request).await {
|
||||
Ok(Ok(result)) if result.get("success").and_then(Value::as_bool) == Some(true) => true,
|
||||
result => {
|
||||
tracing::debug!("tsserver {command}: {result:?}");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LspManager {
|
||||
pub fn set_project_excludes_hook(&self, hook: Box<ExcludesFn>) {
|
||||
*self.excludes_hook.write() = Some(Arc::from(hook));
|
||||
}
|
||||
|
||||
pub(crate) async fn refresh_external_project(&self, server: &Server, run: &Running) {
|
||||
if !supported(server, run) {
|
||||
return;
|
||||
}
|
||||
let mut state = server.external_project.lock().await;
|
||||
if state.generation != run.generation {
|
||||
*state = State { generation: run.generation, files: Vec::new() };
|
||||
server.external_diagnostics.store(false, Ordering::Release);
|
||||
}
|
||||
let root = server.root.clone();
|
||||
let exclude_root = self.project_root_for(&root);
|
||||
let hook = self.excludes_hook.read().clone();
|
||||
let files = tokio::task::spawn_blocking(move || {
|
||||
source_files(&root, &exclude_root, hook.map(|h| h()).unwrap_or_default(), LIMIT)
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if state.files == files {
|
||||
return;
|
||||
}
|
||||
if files.is_empty() {
|
||||
if request(run, "closeExternalProject", json!({ "projectFileName": project_name(server) })).await {
|
||||
state.files.clear();
|
||||
// Keep suppressing delayed diagnostics for unopened files from this instance.
|
||||
}
|
||||
return;
|
||||
}
|
||||
server.external_diagnostics.store(true, Ordering::Release);
|
||||
let roots: Vec<_> = files.iter().map(|p| json!({ "fileName": p })).collect();
|
||||
if request(
|
||||
run,
|
||||
"openExternalProject",
|
||||
json!({
|
||||
"projectFileName": project_name(server), "rootFiles": roots,
|
||||
// What an inferred project assumes, so sharing a project changes no diagnostics.
|
||||
"options": { "allowJs": true, "target": "ES2022", "module": "ESNext",
|
||||
"moduleResolution": "Node", "allowNonTsExtensions": true, "jsx": "preserve",
|
||||
"allowSyntheticDefaultImports": true, "strictNullChecks": true,
|
||||
"strictFunctionTypes": true, "moduleDetection": "force" }
|
||||
}),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::debug!(
|
||||
"{}: registered {} external project files at {}",
|
||||
server.name,
|
||||
files.len(),
|
||||
server.root.display()
|
||||
);
|
||||
state.files = files;
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-registers config-less projects after sources or configurations changed on disk.
|
||||
pub fn project_files_changed<'a>(&self, paths: impl IntoIterator<Item = &'a PathBuf>) {
|
||||
let relevant = paths.into_iter().any(|p| {
|
||||
p.extension().and_then(|s| s.to_str()).is_some_and(is_source)
|
||||
|| p.file_name().is_some_and(|n| n == "tsconfig.json" || n == "jsconfig.json")
|
||||
});
|
||||
if !relevant {
|
||||
return;
|
||||
}
|
||||
for server in self.servers.lock().iter().filter(|s| s.config.tsserver_project) {
|
||||
if server.external_refresh_queued.swap(true, Ordering::AcqRel) {
|
||||
continue;
|
||||
}
|
||||
let (me, server) = (self.me.clone(), server.clone());
|
||||
self.runtime.spawn(async move {
|
||||
tokio::time::sleep(REFRESH_DELAY).await;
|
||||
server.external_refresh_queued.store(false, Ordering::Release);
|
||||
if let (Some(me), Some(run)) = (me.upgrade(), server.running()) {
|
||||
me.refresh_external_project(&server, &run).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn close_external_project(&self, server: &Server, run: &Running) {
|
||||
let mut state = server.external_project.lock().await;
|
||||
if state.generation == run.generation && !state.files.is_empty() {
|
||||
request(run, "closeExternalProject", json!({ "projectFileName": project_name(server) })).await;
|
||||
state.files.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn write(root: &Path, path: &str) {
|
||||
let path = root.join(path);
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(path, "").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selects_sources_honouring_ignores_exclusions_and_roots() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let root = dir.path();
|
||||
for path in [
|
||||
"a.ts",
|
||||
"src/b.js",
|
||||
"src/c.tsx",
|
||||
"readme.md",
|
||||
"node_modules/n.ts",
|
||||
"dist/d.js",
|
||||
"excluded/e.ts",
|
||||
"ignored/i.ts",
|
||||
"other/f.py",
|
||||
] {
|
||||
write(root, path);
|
||||
}
|
||||
std::fs::write(root.join(".gitignore"), "ignored/\n").unwrap();
|
||||
assert_eq!(
|
||||
source_files(root, root, vec!["excluded".into()], LIMIT),
|
||||
vec![root.join("a.ts"), root.join("src/b.js"), root.join("src/c.tsx")]
|
||||
);
|
||||
assert!(source_files(root, root, vec![], 1).is_empty());
|
||||
std::fs::File::create(root.join("large.js")).unwrap().set_len(SCRIPT_BYTES_LIMIT + 1).unwrap();
|
||||
assert!(source_files(root, root, vec![], LIMIT).is_empty());
|
||||
std::fs::remove_file(root.join("large.js")).unwrap();
|
||||
std::fs::File::create(root.join("large.ts")).unwrap().set_len(SCRIPT_BYTES_LIMIT + 1).unwrap();
|
||||
assert_eq!(source_files(root, root, vec!["excluded".into()], LIMIT).len(), 4);
|
||||
std::fs::remove_file(root.join("large.ts")).unwrap();
|
||||
assert_eq!(source_files(&root.join("src"), root, vec![], LIMIT).len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_ancestor_and_nested_configs_keep_existing_projects() {
|
||||
for name in ["tsconfig.json", "jsconfig.json"] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let root = dir.path();
|
||||
write(root, "a.ts");
|
||||
write(root, "pkg/b.js");
|
||||
assert_eq!(source_files(root, root, vec![], LIMIT).len(), 2);
|
||||
write(root, &format!("pkg/{name}"));
|
||||
assert!(source_files(root, root, vec![], LIMIT).is_empty());
|
||||
std::fs::remove_file(root.join("pkg").join(name)).unwrap();
|
||||
write(root, name);
|
||||
assert!(source_files(root, root, vec![], LIMIT).is_empty());
|
||||
assert!(source_files(&root.join("pkg"), root, vec![], LIMIT).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn additions_deletions_and_renames_change_membership() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
write(dir.path(), "a.js");
|
||||
let first = source_files(dir.path(), dir.path(), vec![], LIMIT);
|
||||
write(dir.path(), "b.ts");
|
||||
assert_eq!(source_files(dir.path(), dir.path(), vec![], LIMIT).len(), 2);
|
||||
std::fs::rename(dir.path().join("b.ts"), dir.path().join("b.txt")).unwrap();
|
||||
assert_eq!(source_files(dir.path(), dir.path(), vec![], LIMIT), first);
|
||||
std::fs::remove_file(dir.path().join("a.js")).unwrap();
|
||||
assert!(source_files(dir.path(), dir.path(), vec![], LIMIT).is_empty());
|
||||
}
|
||||
}
|
||||
@@ -120,7 +120,10 @@ impl ClientHandler {
|
||||
};
|
||||
let Some(path) = uri_to_path(params.uri.as_str()) else { return };
|
||||
// Files opened only for a request (references search) are not the user's problems.
|
||||
if self.server.suppresses_diagnostics(&path) {
|
||||
if (self.server.external_diagnostics.load(std::sync::atomic::Ordering::Acquire)
|
||||
&& !mgr.docs.lock().values().any(|d| d.path == path))
|
||||
|| self.server.suppresses_diagnostics(&path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let conv = mgr.conv(self.encoding());
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
mod angular;
|
||||
mod caps;
|
||||
mod convert;
|
||||
mod external_project;
|
||||
mod file_ops;
|
||||
mod handler;
|
||||
mod inspect;
|
||||
@@ -169,6 +170,7 @@ pub struct LspManager {
|
||||
prepare_hook: RwLock<Option<Arc<PrepareFn>>>,
|
||||
servers_hook: RwLock<Option<Arc<ServersFn>>>,
|
||||
candidates_hook: RwLock<Option<Arc<CandidateFn>>>,
|
||||
excludes_hook: RwLock<Option<Arc<external_project::ExcludesFn>>>,
|
||||
shutting_down: AtomicBool,
|
||||
/// `window/showMessage` texts already surfaced; servers repeat them on every restart/reconfigure.
|
||||
shown_messages: Mutex<HashSet<(String, String)>>,
|
||||
@@ -299,6 +301,7 @@ impl LspManager {
|
||||
prepare_hook: RwLock::new(None),
|
||||
servers_hook: RwLock::new(None),
|
||||
candidates_hook: RwLock::new(None),
|
||||
excludes_hook: RwLock::new(None),
|
||||
shutting_down: AtomicBool::new(false),
|
||||
shown_messages: Mutex::new(HashSet::new()),
|
||||
virtual_origins: Mutex::new(HashMap::new()),
|
||||
@@ -517,6 +520,8 @@ impl LspManager {
|
||||
}
|
||||
let run = Running { generation, peer: peer.clone(), caps: Arc::new(RwLock::new(caps)) };
|
||||
|
||||
self.refresh_external_project(&server, &run).await;
|
||||
|
||||
// Publish readiness and (re)open attached documents atomically w.r.t. did_change.
|
||||
{
|
||||
let mut docs = self.docs.lock();
|
||||
@@ -2212,6 +2217,7 @@ impl LspManager {
|
||||
return;
|
||||
}
|
||||
let graceful = async {
|
||||
self.close_external_project(server, &run).await;
|
||||
peer.request::<_, Value>("shutdown", (), None).await.ok();
|
||||
peer.notify("exit", ()).ok();
|
||||
peer.closed().await;
|
||||
|
||||
@@ -99,6 +99,9 @@ pub struct Server {
|
||||
/// Woken when the last `$/progress` task ends.
|
||||
idle_notify: Notify,
|
||||
transient: Mutex<HashMap<PathBuf, Transient>>,
|
||||
pub external_project: tokio::sync::Mutex<crate::external_project::State>,
|
||||
pub external_diagnostics: std::sync::atomic::AtomicBool,
|
||||
pub external_refresh_queued: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
@@ -123,6 +126,9 @@ impl Server {
|
||||
published_notify: Notify::new(),
|
||||
idle_notify: Notify::new(),
|
||||
transient: Mutex::new(HashMap::new()),
|
||||
external_project: tokio::sync::Mutex::new(Default::default()),
|
||||
external_diagnostics: std::sync::atomic::AtomicBool::new(false),
|
||||
external_refresh_queued: std::sync::atomic::AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -210,6 +210,7 @@ fn server_cfg(name: &str) -> LanguageServerConfig {
|
||||
fallback: false,
|
||||
max_java: None,
|
||||
launch_error: None,
|
||||
tsserver_project: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2258,3 +2259,67 @@ async fn rename_edits_files_outside_the_analysis_scope() {
|
||||
assert_eq!(paths, vec![a, b.clone()]);
|
||||
eventually(|| uris(&s.notified("textDocument/didClose")) == vec![uri_of(&b)]).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn external_projects_are_capability_gated_and_follow_lifecycle() {
|
||||
for capable in [false, true] {
|
||||
let mut caps = incremental_caps();
|
||||
caps["executeCommandProvider"] = json!({ "commands": if capable {
|
||||
vec!["typescript.tsserverRequest"]
|
||||
} else { vec!["_typescript.organizeImports"] } });
|
||||
let h = harness_configured("typescript", "ts", &[("arbitrary-name", caps)], |cfg| {
|
||||
cfg.tsserver_project = true;
|
||||
});
|
||||
let s = h.launcher.state("arbitrary-name");
|
||||
s.respond("workspace/executeCommand", json!({ "success": true, "body": true }));
|
||||
std::fs::write(h.root.join("unopened.ts"), "export const helper = 1;").unwrap();
|
||||
h.open("main.ts", "helper");
|
||||
h.wait_status("arbitrary-name", ServerStatus::Ready).await;
|
||||
if !capable {
|
||||
assert!(s.requested("workspace/executeCommand").is_empty());
|
||||
h.mgr.shutdown_all().await;
|
||||
assert!(s.requested("workspace/executeCommand").is_empty());
|
||||
continue;
|
||||
}
|
||||
let requests = s.requested("workspace/executeCommand");
|
||||
assert_eq!(requests[0]["arguments"][0], "openExternalProject");
|
||||
assert_eq!(requests[0]["arguments"][1]["rootFiles"].as_array().unwrap().len(), 2);
|
||||
assert_eq!(s.docs.lock().len(), 1, "registration must not open source documents");
|
||||
std::fs::write(h.root.join("new.ts"), "export const newHelper = 1;").unwrap();
|
||||
h.mgr.project_files_changed([&h.root.join("new.ts")]);
|
||||
eventually(|| s.requested("workspace/executeCommand").len() >= 2).await;
|
||||
assert_eq!(
|
||||
s.requested("workspace/executeCommand")[1]["arguments"][1]["rootFiles"].as_array().unwrap().len(),
|
||||
3
|
||||
);
|
||||
std::fs::rename(h.root.join("new.ts"), h.root.join("renamed.ts")).unwrap();
|
||||
h.mgr.project_files_changed([&h.root.join("renamed.ts")]);
|
||||
eventually(|| s.requested("workspace/executeCommand").len() >= 3).await;
|
||||
assert!(s.requested("workspace/executeCommand")[2].to_string().contains("renamed.ts"));
|
||||
std::fs::remove_file(h.root.join("renamed.ts")).unwrap();
|
||||
h.mgr.project_files_changed([&h.root.join("renamed.ts")]);
|
||||
eventually(|| s.requested("workspace/executeCommand").len() >= 4).await;
|
||||
let id = h.mgr.servers()[0].id;
|
||||
h.mgr.restart(id).await.unwrap();
|
||||
h.wait_status("arbitrary-name", ServerStatus::Ready).await;
|
||||
eventually(|| {
|
||||
s.requested("workspace/executeCommand")
|
||||
.iter()
|
||||
.filter(|r| r["arguments"][0] == "openExternalProject")
|
||||
.count()
|
||||
>= 5
|
||||
})
|
||||
.await;
|
||||
std::fs::write(h.root.join("tsconfig.json"), "{}").unwrap();
|
||||
h.mgr.project_files_changed([&h.root.join("tsconfig.json")]);
|
||||
eventually(|| {
|
||||
s.requested("workspace/executeCommand")
|
||||
.iter()
|
||||
.filter(|r| r["arguments"][0] == "closeExternalProject")
|
||||
.count()
|
||||
>= 2
|
||||
})
|
||||
.await;
|
||||
h.mgr.shutdown_all().await;
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,7 @@ async fn missing_binary_is_reported_with_install_hint() {
|
||||
fallback: false,
|
||||
max_java: None,
|
||||
launch_error: None,
|
||||
tsserver_project: false,
|
||||
};
|
||||
let (mgr, events) = manager(dir.path(), LanguageRegistry::from_configs(vec![lang("nope", "nope", server)]));
|
||||
let path = dir.path().join("a.nope");
|
||||
@@ -247,6 +248,7 @@ async fn typescript_language_server_end_to_end() {
|
||||
fallback: false,
|
||||
max_java: None,
|
||||
launch_error: None,
|
||||
tsserver_project: false,
|
||||
};
|
||||
let (mgr, events) = manager(&root, LanguageRegistry::from_configs(vec![lang("typescript", "ts", server)]));
|
||||
let doc = DocId::next();
|
||||
@@ -292,6 +294,7 @@ fn npm_server(tools: &Path, name: &str, bin: &str, init: Option<serde_json::Valu
|
||||
fallback: false,
|
||||
max_java: None,
|
||||
launch_error: None,
|
||||
tsserver_project: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -462,6 +462,7 @@ fn custom_server(name: &str, ls: &LanguageServerSettings) -> LanguageServerConfi
|
||||
fallback: false,
|
||||
max_java: None,
|
||||
launch_error: None,
|
||||
tsserver_project: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ fn server(name: &str) -> LanguageServerConfig {
|
||||
fallback: false,
|
||||
max_java: None,
|
||||
launch_error: None,
|
||||
tsserver_project: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ tree-sitter = "javascript"
|
||||
|
||||
[[language-servers]]
|
||||
name = "vtsls"
|
||||
tsserver-project = true
|
||||
command = "vtsls"
|
||||
args = ["--stdio"]
|
||||
root-markers = ["jsconfig.json", "tsconfig.json", "package.json"]
|
||||
@@ -17,6 +18,7 @@ install-hint = "npm i -g @vtsls/language-server"
|
||||
|
||||
[[language-servers]]
|
||||
name = "typescript-language-server"
|
||||
tsserver-project = true
|
||||
# Alternative: used only when the server above is not installed.
|
||||
fallback = true
|
||||
command = "typescript-language-server"
|
||||
|
||||
@@ -9,6 +9,7 @@ tree-sitter = "javascript"
|
||||
|
||||
[[language-servers]]
|
||||
name = "vtsls"
|
||||
tsserver-project = true
|
||||
command = "vtsls"
|
||||
args = ["--stdio"]
|
||||
root-markers = ["jsconfig.json", "tsconfig.json", "package.json"]
|
||||
@@ -16,6 +17,7 @@ install-hint = "npm i -g @vtsls/language-server"
|
||||
|
||||
[[language-servers]]
|
||||
name = "typescript-language-server"
|
||||
tsserver-project = true
|
||||
# Alternative: used only when the server above is not installed.
|
||||
fallback = true
|
||||
command = "typescript-language-server"
|
||||
|
||||
@@ -9,6 +9,7 @@ tree-sitter = "typescript"
|
||||
|
||||
[[language-servers]]
|
||||
name = "vtsls"
|
||||
tsserver-project = true
|
||||
command = "vtsls"
|
||||
args = ["--stdio"]
|
||||
root-markers = ["tsconfig.json", "jsconfig.json", "package.json"]
|
||||
@@ -16,6 +17,7 @@ install-hint = "npm i -g @vtsls/language-server"
|
||||
|
||||
[[language-servers]]
|
||||
name = "typescript-language-server"
|
||||
tsserver-project = true
|
||||
# Alternative: used only when the server above is not installed.
|
||||
fallback = true
|
||||
command = "typescript-language-server"
|
||||
|
||||
@@ -9,6 +9,7 @@ tree-sitter = "tsx"
|
||||
|
||||
[[language-servers]]
|
||||
name = "vtsls"
|
||||
tsserver-project = true
|
||||
command = "vtsls"
|
||||
args = ["--stdio"]
|
||||
root-markers = ["tsconfig.json", "jsconfig.json", "package.json"]
|
||||
@@ -16,6 +17,7 @@ install-hint = "npm i -g @vtsls/language-server"
|
||||
|
||||
[[language-servers]]
|
||||
name = "typescript-language-server"
|
||||
tsserver-project = true
|
||||
# Alternative: used only when the server above is not installed.
|
||||
fallback = true
|
||||
command = "typescript-language-server"
|
||||
|
||||
Reference in new issue
Заблокувати користувача