mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-13 04:34:21 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
953b51b8c6 |
No files matched your search
@@ -1097,8 +1097,6 @@ impl AgentManager {
|
||||
title_renamed: s.title_renamed,
|
||||
}
|
||||
};
|
||||
// Copying may have started the source's agent: it stops again once idle.
|
||||
source.release_when_idle();
|
||||
let task = Task::restore(file, self.shared.clone());
|
||||
self.tasks.write().insert(task.id.clone(), task.clone());
|
||||
task.persist();
|
||||
@@ -1168,8 +1166,6 @@ impl AgentManager {
|
||||
task.persist();
|
||||
self.shared.emit(AgentEvent::Truncated { task: id.to_owned(), length: cut as u32 });
|
||||
task.emit_task();
|
||||
// Undoing may have started the agent: it stops again once idle.
|
||||
task.release_when_idle();
|
||||
Ok(first)
|
||||
}
|
||||
|
||||
|
||||
@@ -583,12 +583,11 @@ impl Shim {
|
||||
impl super::Shim for Shim {
|
||||
fn attach(&self, client: Peer) {
|
||||
let _ = self.client.set(client.clone());
|
||||
// The client went away: so do the processes. Held until then: the connection drops
|
||||
// its handler (this shim) as it closes, and the sessions would go without stopping.
|
||||
let me = self.me.upgrade();
|
||||
// The client went away: so do the processes.
|
||||
let weak = self.me.clone();
|
||||
tokio::spawn(async move {
|
||||
client.closed().await;
|
||||
if let Some(me) = me {
|
||||
if let Some(me) = weak.upgrade() {
|
||||
me.closed.store(true, Ordering::Release);
|
||||
let sessions: Vec<Arc<Session>> = me.sessions.lock().drain().map(|(_, s)| s).collect();
|
||||
for s in sessions {
|
||||
@@ -599,15 +598,6 @@ impl super::Shim for Shim {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Session {
|
||||
/// A session dropped without being stopped (the shim went away) takes its process along.
|
||||
fn drop(&mut self) {
|
||||
if let Some(p) = self.st.get_mut().proc.take() {
|
||||
p.kill.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
fn shim(&self) -> Option<Arc<Shim>> {
|
||||
self.shim.upgrade()
|
||||
|
||||
@@ -311,8 +311,6 @@ struct Replay {
|
||||
sent: Arc<Mutex<Vec<Vec<Value>>>>,
|
||||
launched: Mutex<Vec<Vec<String>>>,
|
||||
errors: Arc<Mutex<Vec<String>>>,
|
||||
/// Each process's kill token.
|
||||
kills: Mutex<Vec<CancellationToken>>,
|
||||
}
|
||||
|
||||
impl Cli for Replay {
|
||||
@@ -333,7 +331,6 @@ impl Cli for Replay {
|
||||
let (shim_in, cli_in) = tokio::io::duplex(1 << 20);
|
||||
let (mut cli_out, shim_out) = tokio::io::duplex(1 << 20);
|
||||
let kill = CancellationToken::new();
|
||||
self.kills.lock().push(kill.clone());
|
||||
let (sent, errors, token) = (self.sent.clone(), self.errors.clone(), kill.clone());
|
||||
sent.lock().push(Vec::new());
|
||||
tokio::spawn(async move {
|
||||
@@ -431,7 +428,6 @@ async fn replay(name: &str) -> Replayed {
|
||||
sent: Arc::default(),
|
||||
launched: Mutex::default(),
|
||||
errors: Arc::default(),
|
||||
kills: Mutex::default(),
|
||||
});
|
||||
let (config, cwd) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
|
||||
let cwd_path = std::fs::canonicalize(cwd.path()).unwrap();
|
||||
@@ -465,23 +461,6 @@ async fn replay_simple() {
|
||||
assert_eq!(user["message"]["content"][0]["text"], "Reply with exactly: hello there");
|
||||
}
|
||||
|
||||
/// The app closing the connection (an idle task released, archived or deleted) stops the
|
||||
/// CLI processes of its sessions, idle ones included.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn closing_the_connection_stops_the_processes() {
|
||||
let r = replay("simple").await;
|
||||
assert!(r.cli.kills.lock().iter().all(|k| !k.is_cancelled()));
|
||||
r.h.client.kill();
|
||||
let kills = r.cli.kills.lock().clone();
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
for k in kills {
|
||||
k.cancelled().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("every process was stopped");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn replay_multi_turn() {
|
||||
let r = replay("multi_turn").await;
|
||||
@@ -748,7 +727,6 @@ async fn a_stop_ahead_of_its_prompt_cancels_it() {
|
||||
sent: Arc::default(),
|
||||
launched: Mutex::default(),
|
||||
errors: Arc::default(),
|
||||
kills: Mutex::default(),
|
||||
});
|
||||
let (dir, cwd) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
|
||||
let h = Harness::connect(cli.clone(), dir.path().into(), dir.path().join("p"), cwd.path().into(), Vec::new()).await;
|
||||
@@ -792,7 +770,6 @@ async fn a_failed_turn_reports_its_failure() {
|
||||
sent: Arc::default(),
|
||||
launched: Mutex::default(),
|
||||
errors: Arc::default(),
|
||||
kills: Mutex::default(),
|
||||
});
|
||||
let (dir, cwd) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
|
||||
let h =
|
||||
|
||||
@@ -2391,27 +2391,6 @@ async fn idle_agents_stop_and_resume_their_session_on_the_next_message() {
|
||||
assert!(live(&mgr, &task.id));
|
||||
mgr.shutdown();
|
||||
|
||||
// Undoing turns or copying the conversation starts a stopped agent: it stops again too.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let agent = Arc::new(FakeAgent::default());
|
||||
let mgr = manager(tmp.path(), agent.clone());
|
||||
mgr.set_idle_release(Duration::from_millis(200));
|
||||
let task = mgr.create(new()).await.unwrap();
|
||||
wait_for(&mgr, &task.id, AgentStatus::Ready).await;
|
||||
mgr.prompt(&task.id, text("again"), false).await.unwrap();
|
||||
wait_for(&mgr, &task.id, AgentStatus::Ready).await;
|
||||
tokio::time::sleep(Duration::from_millis(600)).await;
|
||||
assert!(!live(&mgr, &task.id));
|
||||
mgr.rollback(&task.id, 1).await.unwrap();
|
||||
assert!(live(&mgr, &task.id));
|
||||
tokio::time::sleep(Duration::from_millis(600)).await;
|
||||
assert!(!live(&mgr, &task.id), "an agent started to undo turns stops too");
|
||||
mgr.fork(&task.id, None).await.unwrap();
|
||||
assert!(live(&mgr, &task.id));
|
||||
tokio::time::sleep(Duration::from_millis(600)).await;
|
||||
assert!(!live(&mgr, &task.id), "an agent started to copy the conversation stops too");
|
||||
mgr.shutdown();
|
||||
|
||||
// An agent that can't reload its session keeps running: stopping it would lose the conversation.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let agent = Arc::new(FakeAgent { no_load: true, ..FakeAgent::default() });
|
||||
|
||||
@@ -15,7 +15,6 @@ use ide_build::{BuildModel, BuildTool, SyncOptions, SyncProgress};
|
||||
use ide_core::settings::Settings;
|
||||
use ide_core::{DocId, FileEdit, IdeError, IdeEvent, IdeResult, Location, Pos, Range, RequestId, WorkspaceEdit};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn join_err(e: tokio::task::JoinError) -> IdeError {
|
||||
IdeError::internal(e.to_string())
|
||||
@@ -331,61 +330,21 @@ pub async fn build_sync(
|
||||
// holds for Gradle: it reuses its cached classpath on restart, so a
|
||||
// dependency added to `build.gradle` stays unresolved until told to sync.
|
||||
let lsp = project.lsp.clone();
|
||||
let documents = project.documents.clone();
|
||||
tokio::spawn(async move {
|
||||
if profiles_changed {
|
||||
// jdtls takes the profiles from its Maven settings (see `ide_sdk::maven_profiles`).
|
||||
lsp.reconfigure().await;
|
||||
}
|
||||
let first = updates.first().cloned();
|
||||
for file in updates {
|
||||
let uri = ide_lsp::path_to_uri_string(&file);
|
||||
if let Err(e) = lsp.java_update_project_configs(&file, vec![uri]).await {
|
||||
tracing::debug!("java/projectConfigurationsUpdate: {e}");
|
||||
}
|
||||
}
|
||||
let changed = {
|
||||
let lsp = lsp.clone();
|
||||
tokio::task::spawn_blocking(move || lsp.changed_library_archives()).await.unwrap_or_default()
|
||||
};
|
||||
if let Some(file) = first
|
||||
&& let Err(e) = lsp.java_refresh_archives(&file, &changed).await
|
||||
{
|
||||
tracing::debug!("openidea.java.refreshArchives: {e}");
|
||||
}
|
||||
refresh_library_classes(&lsp, &documents).await;
|
||||
});
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
/// Open library classes whose jar was replaced in place (a SNAPSHOT installed
|
||||
/// again keeps its path) show the new contents, like a reopened one.
|
||||
async fn refresh_library_classes(lsp: &Arc<ide_lsp::LspManager>, documents: &Arc<ide_workspace::Documents>) {
|
||||
let changed = {
|
||||
let (lsp, documents) = (lsp.clone(), documents.clone());
|
||||
tokio::task::spawn_blocking(move || {
|
||||
documents
|
||||
.open_documents()
|
||||
.into_iter()
|
||||
.filter_map(|doc| Some((doc, documents.virtual_uri(doc).ok().flatten()?)))
|
||||
.filter(|(_, uri)| lsp.library_class_changed(uri))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
};
|
||||
for (doc, uri) in changed {
|
||||
match lsp.virtual_document(&uri).await {
|
||||
Ok(vd) => {
|
||||
if let Err(e) = documents.replace_virtual(doc, &vd.text, &vd.encoding, vd.has_bom) {
|
||||
tracing::debug!("refreshing {uri}: {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::debug!("refreshing {uri}: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The POM declaring `dependency` for the Maven module `pom` (project-relative): the
|
||||
/// module's own, else its nearest parent in the project; its path, text and declaration.
|
||||
fn declaring_pom(
|
||||
@@ -1092,6 +1051,7 @@ mod maven_completion_tests {
|
||||
use ide_build::maven::completion::PomCompletionKind;
|
||||
use ide_core::CancellationToken;
|
||||
use ide_workspace::dependencies::DependencyVersionSource;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Cache answers the test plants before completing, so the op's Central
|
||||
/// lookups hit the session cache and the network is never reached.
|
||||
|
||||
@@ -809,21 +809,6 @@ impl LspManager {
|
||||
.map_err(|e| crate::rpc_err(&server.name, "java/projectConfigurationsUpdate", e))
|
||||
}
|
||||
|
||||
/// Asks jdtls to re-read library archives replaced in place (`openidea.java.refreshArchives`):
|
||||
/// a SNAPSHOT installed again keeps its path, and JDT would keep showing the classes it read
|
||||
/// from the old archive. Best-effort: skipped while jdtls is not already running.
|
||||
pub async fn java_refresh_archives(&self, path: &Path, archives: &[PathBuf]) -> IdeResult<()> {
|
||||
let java = LanguageId::new("java");
|
||||
let Some(server) = self.servers_for(&java, path).into_iter().find(|s| s.name == SERVER) else {
|
||||
return Ok(());
|
||||
};
|
||||
if server.running().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let archives = archives.iter().map(|a| Value::String(a.to_string_lossy().into_owned())).collect();
|
||||
self.jdtls_command(path, "openidea.java.refreshArchives", archives).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// Pause autobuild before resolving the launch classpath. That query takes the
|
||||
/// Java project's workspace scheduling rule, draining the interrupted autobuild.
|
||||
/// Keep this guard until Maven outputs have been copied out of JDT's reach.
|
||||
|
||||
@@ -241,9 +241,6 @@ pub struct LspManager {
|
||||
virtual_origins: Mutex<HashMap<String, ide_core::ServerId>>,
|
||||
/// Contents fetched for virtual URIs.
|
||||
virtual_docs: Mutex<HashMap<String, VirtualDocument>>,
|
||||
/// Modification time of the archive behind a cached library class, so a jar
|
||||
/// replaced in place (a SNAPSHOT installed again) is read again.
|
||||
virtual_stamps: Mutex<HashMap<String, std::time::SystemTime>>,
|
||||
injections: injections::State,
|
||||
/// Open notebooks by path. Locked before `docs` when both are.
|
||||
notebooks: Mutex<HashMap<PathBuf, notebook::NotebookState>>,
|
||||
@@ -511,7 +508,6 @@ impl LspManager {
|
||||
go_mod_checked: Mutex::new(HashSet::new()),
|
||||
virtual_origins: Mutex::new(HashMap::new()),
|
||||
virtual_docs: Mutex::new(HashMap::new()),
|
||||
virtual_stamps: Mutex::new(HashMap::new()),
|
||||
injections: injections::State::default(),
|
||||
notebooks: Mutex::new(HashMap::new()),
|
||||
})
|
||||
@@ -3206,44 +3202,14 @@ impl LspManager {
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
/// Whether the archive behind a cached library class changed since it was read.
|
||||
/// Stats the archive: call from a blocking worker.
|
||||
pub fn library_class_changed(&self, uri: &str) -> bool {
|
||||
let Some(stamp) = self.virtual_stamps.lock().get(uri).copied() else { return false };
|
||||
virtual_doc::archive_modified(uri) != Some(stamp)
|
||||
}
|
||||
|
||||
/// Archives behind cached library classes that changed since they were read.
|
||||
/// Stats each archive: call from a blocking worker.
|
||||
pub fn changed_library_archives(&self) -> Vec<PathBuf> {
|
||||
let uris: Vec<String> = self.virtual_stamps.lock().keys().cloned().collect();
|
||||
let mut archives: Vec<PathBuf> = uris
|
||||
.iter()
|
||||
.filter(|uri| self.library_class_changed(uri))
|
||||
.filter_map(|uri| virtual_doc::archive(uri))
|
||||
.collect();
|
||||
archives.sort();
|
||||
archives.dedup();
|
||||
archives
|
||||
}
|
||||
|
||||
/// Contents of a virtual document, from the server that owns `uri`:
|
||||
/// `workspace/textDocumentContent` (LSP 3.18) when the server serves the
|
||||
/// scheme, jdtls' `java/classFileContents` for `jdt:` URIs. A `jar:` entry
|
||||
/// is read from its archive, a `jar:` class shown as its library source
|
||||
/// (see [`library_source`]) or else decompiled by the server. Cached.
|
||||
pub async fn virtual_document(&self, uri: &str) -> IdeResult<VirtualDocument> {
|
||||
let cached = self.virtual_docs.lock().get(uri).cloned();
|
||||
if let Some(d) = cached {
|
||||
let stamp = self.virtual_stamps.lock().get(uri).copied();
|
||||
let current = {
|
||||
let uri = uri.to_owned();
|
||||
tokio::task::spawn_blocking(move || virtual_doc::archive_modified(&uri)).await.ok().flatten()
|
||||
};
|
||||
if stamp.is_none() || stamp == current {
|
||||
return Ok(d);
|
||||
}
|
||||
self.virtual_docs.lock().remove(uri);
|
||||
if let Some(d) = self.virtual_docs.lock().get(uri) {
|
||||
return Ok(d.clone());
|
||||
}
|
||||
let scheme = virtual_doc::scheme(uri).to_owned();
|
||||
let jar_class = scheme == "jar" && library_source::parse(uri).is_some_and(|e| e.entry.ends_with(".class"));
|
||||
@@ -3387,18 +3353,11 @@ impl LspManager {
|
||||
if let Some(server) = server {
|
||||
self.virtual_origins.lock().entry(uri.to_owned()).or_insert(server);
|
||||
}
|
||||
let stamp = virtual_doc::archive_modified(uri);
|
||||
let mut cache = self.virtual_docs.lock();
|
||||
let mut stamps = self.virtual_stamps.lock();
|
||||
if cache.len() >= VIRTUAL_CACHE_LIMIT {
|
||||
cache.clear();
|
||||
stamps.clear();
|
||||
}
|
||||
cache.insert(uri.to_owned(), doc.clone());
|
||||
match stamp {
|
||||
Some(stamp) => stamps.insert(uri.to_owned(), stamp),
|
||||
None => stamps.remove(uri),
|
||||
};
|
||||
doc
|
||||
}
|
||||
|
||||
|
||||
@@ -172,20 +172,6 @@ pub fn description(uri: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The archive a `jar:` or `jdt:` library class comes from.
|
||||
pub(crate) fn archive(uri: &str) -> Option<std::path::PathBuf> {
|
||||
match scheme(uri) {
|
||||
"jar" => Some(crate::library_source::parse(uri)?.jar),
|
||||
"jdt" => description(uri).filter(|p| p.ends_with(".jar") || p.ends_with(".jmod")).map(Into::into),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Modification time of the archive a `jar:` or `jdt:` library class comes from.
|
||||
pub(crate) fn archive_modified(uri: &str) -> Option<std::time::SystemTime> {
|
||||
std::fs::metadata(archive(uri)?).and_then(|m| m.modified()).ok()
|
||||
}
|
||||
|
||||
/// Check both the package and actual archive, so duplicate type names never pick another jar.
|
||||
pub(crate) fn matches_class(uri: &str, jar: &std::path::Path, package: &str, name: &str) -> bool {
|
||||
let segs = segments(uri);
|
||||
@@ -318,29 +304,6 @@ mod tests {
|
||||
assert_eq!(description("foo://x/y"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_the_archive_behind_a_library_class() {
|
||||
assert_eq!(archive(JDT), Some("/Users/me/.m2/repository/spring-boot-4.1.1.jar".into()));
|
||||
assert_eq!(
|
||||
archive("jdt://contents/java.base/java.lang/String.class?=p/%5C/jdk%5C/lib%5C/jrt-fs.jar%60java.base=x"),
|
||||
None
|
||||
);
|
||||
assert_eq!(archive("jdt://contents/a.jar/p/C.class"), None);
|
||||
assert_eq!(archive("file:///a/C.java"), None);
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let jar = dir.path().join("lib-1.0-SNAPSHOT.jar");
|
||||
std::fs::write(&jar, b"PK").unwrap();
|
||||
let uri = format!("jar:file://{}!/p/C.class", jar.display());
|
||||
assert_eq!(archive(&uri), Some(jar.clone()));
|
||||
let before = archive_modified(&uri).unwrap();
|
||||
let file = std::fs::File::options().write(true).open(&jar).unwrap();
|
||||
file.set_modified(before + std::time::Duration::from_secs(5)).unwrap();
|
||||
assert_ne!(archive_modified(&uri), Some(before));
|
||||
std::fs::remove_file(&jar).unwrap();
|
||||
assert_eq!(archive_modified(&uri), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_generated_code() {
|
||||
assert!(is_decompiled(
|
||||
|
||||
@@ -1547,7 +1547,7 @@ pub fn usages_bundle(data_dir: &Path) -> Option<PathBuf> {
|
||||
install_bundle(
|
||||
data_dir,
|
||||
"java-usages",
|
||||
"1.14.0",
|
||||
"1.13.0",
|
||||
include_bytes!("../../../java/java-usages/openidea-java-usages.jar"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -122,23 +122,6 @@ test("Apply writes and stages unresolved text only after confirmation", async ({
|
||||
expect((await writes(page))[0]!.args[2]).toEqual({ changes: [{ from: 0, to: 13, insert: BASE }] });
|
||||
});
|
||||
|
||||
test("footer buttons work when a click doesn't focus them, as in WebKit", async ({ page }) => {
|
||||
await mountMerge(page);
|
||||
await result(page).locator(".cm-content").click();
|
||||
// WebKit focuses the clicked button's nearest focusable ancestor, not the button.
|
||||
await page
|
||||
.locator("#merge-host")
|
||||
.getByRole("button", { name: "Apply", exact: true })
|
||||
.evaluate((button) => {
|
||||
let target = button.parentElement;
|
||||
while (target && !target.hasAttribute("tabindex")) target = target.parentElement;
|
||||
if (target) target.focus();
|
||||
else (document.activeElement as HTMLElement | null)?.blur();
|
||||
(button as HTMLButtonElement).click();
|
||||
});
|
||||
await expect(page.getByRole("dialog")).toContainText("Save and mark the file as resolved anyway?");
|
||||
});
|
||||
|
||||
test("binary and unreadable versions show an error with no Apply controls", async ({ page }) => {
|
||||
await mountMerge(page, ["\0base", "\0ours", "\0theirs"]);
|
||||
await expect(page.locator("#merge-host")).toContainText("Binary files cannot be resolved in the merge editor.");
|
||||
|
||||
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
|
||||
Bundle-ManifestVersion: 2
|
||||
Bundle-Name: OpenIDEA Java usages
|
||||
Bundle-SymbolicName: ai.openidea.usages;singleton:=true
|
||||
Bundle-Version: 1.14.0
|
||||
Bundle-Version: 1.13.0
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Require-Bundle: org.eclipse.jdt.ls.core,org.eclipse.jdt.core,
|
||||
org.eclipse.jdt.core.manipulation,org.eclipse.core.runtime,
|
||||
|
||||
Binary file not shown.
@@ -17,9 +17,6 @@
|
||||
<delegateCommandHandler class="ai.openidea.implementations.ImplementationsCommand">
|
||||
<command id="openidea.java.implementations"/>
|
||||
</delegateCommandHandler>
|
||||
<delegateCommandHandler class="ai.openidea.archives.RefreshArchivesCommand">
|
||||
<command id="openidea.java.refreshArchives"/>
|
||||
</delegateCommandHandler>
|
||||
<delegateCommandHandler class="ai.openidea.hierarchy.HierarchyCommand">
|
||||
<command id="openidea.java.hierarchy.members"/>
|
||||
<command id="openidea.java.hierarchy.refactor"/>
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package ai.openidea.archives;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.eclipse.core.resources.ResourcesPlugin;
|
||||
import org.eclipse.core.runtime.IPath;
|
||||
import org.eclipse.core.runtime.IProgressMonitor;
|
||||
import org.eclipse.core.runtime.OperationCanceledException;
|
||||
import org.eclipse.core.runtime.Path;
|
||||
import org.eclipse.jdt.core.IJavaModel;
|
||||
import org.eclipse.jdt.core.IJavaProject;
|
||||
import org.eclipse.jdt.core.IPackageFragmentRoot;
|
||||
import org.eclipse.jdt.core.JavaCore;
|
||||
import org.eclipse.jdt.ls.core.internal.IDelegateCommandHandler;
|
||||
import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin;
|
||||
|
||||
/**
|
||||
* Re-reads library archives replaced in place (a SNAPSHOT installed again keeps its path). The
|
||||
* arguments are the archives' file system paths. Their package fragment roots are closed, which
|
||||
* drops the class files read from the old archive, and jdtls' decompiled sources (cached by class
|
||||
* handle, which a replaced archive keeps) are dropped, so the classes are read again.
|
||||
*/
|
||||
public final class RefreshArchivesCommand implements IDelegateCommandHandler {
|
||||
@Override
|
||||
public Object executeCommand(String command, List<Object> arguments, IProgressMonitor monitor) throws Exception {
|
||||
IJavaModel model = JavaCore.create(ResourcesPlugin.getWorkspace().getRoot());
|
||||
model.refreshExternalArchives(null, monitor);
|
||||
Set<IPath> archives = new HashSet<>();
|
||||
for (Object argument : arguments) {
|
||||
if (argument instanceof String path) archives.add(Path.fromOSString(path));
|
||||
}
|
||||
int closed = 0;
|
||||
if (archives.isEmpty()) return closed;
|
||||
clearDecompilerCache();
|
||||
for (IJavaProject project : model.getJavaProjects()) {
|
||||
for (IPackageFragmentRoot root : project.getPackageFragmentRoots()) {
|
||||
if (monitor.isCanceled()) throw new OperationCanceledException();
|
||||
if (root.isArchive() && root.isExternal() && archives.contains(root.getPath()) && root.isOpen()) {
|
||||
root.close();
|
||||
closed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return closed;
|
||||
}
|
||||
|
||||
/** jdtls keeps decompiled sources in a static per-decompiler LRU with no API to drop them. */
|
||||
private static void clearDecompilerCache() {
|
||||
try {
|
||||
Class<?> impl = IDelegateCommandHandler.class.getClassLoader()
|
||||
.loadClass("org.eclipse.jdt.ls.core.internal.decompiler.DecompilerImpl");
|
||||
Field field = impl.getDeclaredField("decompilerCache");
|
||||
field.setAccessible(true);
|
||||
if (field.get(null) instanceof Map<?, ?> caches) {
|
||||
synchronized (caches) {
|
||||
for (Object cache : caches.values()) {
|
||||
if (cache instanceof Map<?, ?> map) map.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ReflectiveOperationException | RuntimeException e) {
|
||||
JavaLanguageServerPlugin.logException("Clearing decompiled sources", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { create } from "zustand";
|
||||
import { registerActions } from "@/platform/actions";
|
||||
import { call, commands, isCancelled, nextRequestId, onIdeEvent, type RequestId } from "@/platform/ipc";
|
||||
import { call, commands, isCancelled, nextRequestId, type RequestId } from "@/platform/ipc";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { invalidateVirtualDocument, invalidateVirtualDocuments } from "@/platform/virtualDocs";
|
||||
import { invalidateVirtualDocument } from "@/platform/virtualDocs";
|
||||
import { maybeEditorService, notifications } from "@/workbench/contributions";
|
||||
|
||||
interface SourceState {
|
||||
@@ -82,9 +82,6 @@ export function registerLibrarySourcesActions(): () => void {
|
||||
const offProject = useProject.subscribe((state, previous) => {
|
||||
if (state.project !== previous.project) clear();
|
||||
});
|
||||
// A reload may have replaced a library jar in place (a SNAPSHOT installed again): reopened
|
||||
// classes fetch their contents again (the backend refreshes the open ones).
|
||||
const offRoots = onIdeEvent("projectRootsChanged", () => invalidateVirtualDocuments());
|
||||
const offActions = registerActions([
|
||||
{
|
||||
id: "editor.downloadSources",
|
||||
@@ -121,7 +118,6 @@ export function registerLibrarySourcesActions(): () => void {
|
||||
return () => {
|
||||
offActions();
|
||||
offProject();
|
||||
offRoots();
|
||||
clear();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Channel } from "@tauri-apps/api/core";
|
||||
import { EditorSelection, MapMode, StateEffect, StateField, type Extension, type Text } from "@codemirror/state";
|
||||
import { EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
|
||||
import { codeLenses, shownAgain, type CodeLens } from "@/editor/api";
|
||||
import { executeAction, getAction } from "@/platform/actions";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { commands, nextRequestId, type OutlineItem, type RequestId, type SymbolKind } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { debounce, docOf, isUnavailable, lspRequest } from "./lsp";
|
||||
@@ -106,12 +106,6 @@ const lensField = StateField.define<CodeLens[]>({
|
||||
const usagesEnabled = () => editorPrefs().inlay.codeVisionUsages;
|
||||
const authorEnabled = () => editorPrefs().inlay.codeVisionAuthor;
|
||||
|
||||
/** The author hint opens the file's blame annotations, and leaves them open when shown. */
|
||||
function showAnnotations(view: EditorView) {
|
||||
view.focus();
|
||||
if (!getAction("vcs.annotate")?.toggled?.()) void executeAction("vcs.annotate");
|
||||
}
|
||||
|
||||
function openUsages(view: EditorView, offset: number) {
|
||||
view.dispatch({ selection: EditorSelection.cursor(offset), userEvent: "select.lens" });
|
||||
view.focus();
|
||||
@@ -264,7 +258,6 @@ class UsagesLensPlugin {
|
||||
icon: "user",
|
||||
order: 20,
|
||||
kind: "author",
|
||||
run: showAnnotations,
|
||||
});
|
||||
const n = usages ? this.cache.get(key(t)) : undefined;
|
||||
if (n === undefined || (n === 0 && !editorPrefs().inlay.codeVisionNoUsages)) continue;
|
||||
|
||||
@@ -235,16 +235,12 @@ const lintTheme = EditorView.theme({
|
||||
".cm-lintRange-error": {
|
||||
textDecoration: "var(--editor-error-decoration, underline wavy var(--danger))",
|
||||
WebkitTextDecoration: "var(--editor-error-decoration, underline wavy var(--danger))",
|
||||
fontWeight: "var(--wave-error-bold, inherit)",
|
||||
fontStyle: "var(--wave-error-italic, inherit)",
|
||||
textDecorationSkipInk: "none",
|
||||
textUnderlineOffset: "3px",
|
||||
},
|
||||
".cm-lintRange-warning": {
|
||||
textDecoration: "var(--editor-warning-decoration, underline wavy var(--warning))",
|
||||
WebkitTextDecoration: "var(--editor-warning-decoration, underline wavy var(--warning))",
|
||||
fontWeight: "var(--wave-warning-bold, inherit)",
|
||||
fontStyle: "var(--wave-warning-italic, inherit)",
|
||||
backgroundColor: "var(--editor-warning-background, transparent)",
|
||||
textDecorationSkipInk: "none",
|
||||
textUnderlineOffset: "3px",
|
||||
@@ -258,10 +254,8 @@ const lintTheme = EditorView.theme({
|
||||
".cm-lintRange-hint": { textDecoration: "none" },
|
||||
// Typo level: IDEA's green wavy underline.
|
||||
".cm-lintRange.cm-lsp-typo": {
|
||||
textDecoration: "var(--wave-typo-effect, underline wavy var(--wave-typo, var(--success)))",
|
||||
WebkitTextDecoration: "var(--wave-typo-effect, underline wavy var(--wave-typo, var(--success)))",
|
||||
fontWeight: "var(--wave-typo-bold, inherit)",
|
||||
fontStyle: "var(--wave-typo-italic, inherit)",
|
||||
textDecoration: "underline wavy color-mix(in srgb, var(--success) 85%, transparent)",
|
||||
WebkitTextDecoration: "underline wavy color-mix(in srgb, var(--success) 85%, transparent)",
|
||||
textDecorationSkipInk: "none",
|
||||
textUnderlineOffset: "3px",
|
||||
},
|
||||
@@ -269,16 +263,9 @@ const lintTheme = EditorView.theme({
|
||||
textDecoration: "none",
|
||||
backgroundColor: "transparent",
|
||||
color: "var(--fg-unused, var(--fg-faint))",
|
||||
fontWeight: "var(--fg-unused-bold, inherit)",
|
||||
fontStyle: "var(--fg-unused-italic, inherit)",
|
||||
},
|
||||
".cm-lintRange.cm-lsp-unnecessary *": { color: "var(--fg-unused, var(--fg-faint))" },
|
||||
".cm-lintRange.cm-lsp-deprecated": {
|
||||
textDecoration: "var(--fg-deprecated-effect, line-through var(--fg-deprecated, var(--fg-muted)))",
|
||||
WebkitTextDecoration: "var(--fg-deprecated-effect, line-through var(--fg-deprecated, var(--fg-muted)))",
|
||||
fontWeight: "var(--fg-deprecated-bold, inherit)",
|
||||
fontStyle: "var(--fg-deprecated-italic, inherit)",
|
||||
},
|
||||
".cm-lintRange.cm-lsp-deprecated": { textDecoration: "line-through", textDecorationColor: "var(--fg-muted)" },
|
||||
// An error or warning on the same range (merged into one mark) keeps its underline.
|
||||
".cm-lintRange-error.cm-lsp-typo, .cm-lintRange-error.cm-lsp-unnecessary": {
|
||||
textDecoration: "var(--editor-error-decoration, underline wavy var(--danger))",
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
initProjectTree,
|
||||
leadSelection,
|
||||
reloadAll,
|
||||
reloadArchiveDirs,
|
||||
reloadLoadedDirs,
|
||||
setChangedFiles,
|
||||
setOpenFiles,
|
||||
@@ -254,7 +253,6 @@ export function activate(): () => void {
|
||||
initProjectTypes(),
|
||||
onIdeEvent("fsChanged", (e) => void applyFsChange(e)),
|
||||
onIdeEvent("excludedDirsChanged", (e) => void reloadLoadedDirs(e.dirs)),
|
||||
onIdeEvent("projectRootsChanged", () => void reloadArchiveDirs()),
|
||||
];
|
||||
return () => {
|
||||
disposers.reverse().forEach((d) => d());
|
||||
|
||||
@@ -3,7 +3,6 @@ import { MemoryFs, setProjectFsForTests } from "./fs";
|
||||
import { visibleInfo } from "./model";
|
||||
import {
|
||||
applyFsChange,
|
||||
reloadArchiveDirs,
|
||||
reloadLoadedDirs,
|
||||
currentModel,
|
||||
expandAll,
|
||||
@@ -115,28 +114,6 @@ describe("refresh of excluded folders", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("refresh of library archives", () => {
|
||||
it("re-reads the loaded folders inside archives only", async () => {
|
||||
await fs.createFile("/m2/lib.jar!/p/A.class");
|
||||
await loadDir(ROOT);
|
||||
await loadDir("/m2/lib.jar!");
|
||||
await loadDir("/m2/lib.jar!/p");
|
||||
await fs.createFile("/m2/lib.jar!/p/B.class");
|
||||
const spy = vi.spyOn(fs, "readDir");
|
||||
|
||||
await reloadArchiveDirs();
|
||||
|
||||
expect(spy.mock.calls.map(([p]) => p).sort()).toEqual(["/m2/lib.jar!", "/m2/lib.jar!/p"]);
|
||||
expect(
|
||||
useProjectTree
|
||||
.getState()
|
||||
.listings.get("/m2/lib.jar!/p")
|
||||
?.map((e) => e.name),
|
||||
).toEqual(["A.class", "B.class"]);
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("refresh on ignore-rule changes", () => {
|
||||
it("refreshes loaded descendants once, preserving selection and unrelated folders", async () => {
|
||||
await fs.createFile("/p/docs/sub/note.md");
|
||||
|
||||
@@ -565,16 +565,6 @@ export async function reloadLoadedDirs(dirs: readonly string[]) {
|
||||
await Promise.all(dirs.filter((p) => listings.has(p)).map((p) => loadDir(p, true)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-reads the loaded folders inside archives (`<archive>!…`): a build tool sync may have replaced
|
||||
* a library jar in place (a SNAPSHOT installed again), which no file watcher reports.
|
||||
*/
|
||||
export async function reloadArchiveDirs() {
|
||||
const { listings } = get();
|
||||
const inArchive = (p: string) => p.endsWith("!") || p.includes("!/") || p.includes("!\\");
|
||||
await Promise.all([...listings.keys()].filter(inArchive).map((p) => loadDir(p, true)));
|
||||
}
|
||||
|
||||
/** Re-reads every loaded folder (View › Reload from Disk). */
|
||||
export async function reloadAll() {
|
||||
const { listings } = get();
|
||||
|
||||
@@ -10,7 +10,6 @@ import { fontWeightVars, type CustomColorSchemes, type TextAttributesOverride }
|
||||
import {
|
||||
ATTRIBUTES,
|
||||
SCHEMES,
|
||||
EFFECT_CSS,
|
||||
overrideDeclarations,
|
||||
effectiveScheme,
|
||||
resolvedSchemeTokens,
|
||||
@@ -124,8 +123,6 @@ function previewVars(
|
||||
k.startsWith("--border-") ||
|
||||
k.startsWith("--ansi-") ||
|
||||
k.startsWith("--git-") ||
|
||||
k.startsWith("--wave-") ||
|
||||
k.startsWith("--editor-") ||
|
||||
k === "--caret"
|
||||
)
|
||||
vars[k] = v;
|
||||
@@ -140,32 +137,20 @@ function previewVars(
|
||||
|
||||
/** Text styled like an attribute in the preview. */
|
||||
function tokenStyle(attr: AttributeDef | undefined): CSSProperties {
|
||||
const base = attr?.fg ?? attr?.effect;
|
||||
if (!attr?.token) {
|
||||
const colour: CSSProperties = {
|
||||
if (!attr?.token)
|
||||
return {
|
||||
...(attr?.fg ? { color: `var(${attr.fg})` } : null),
|
||||
...(attr?.bg ? { backgroundColor: `var(${attr.bg})` } : null),
|
||||
};
|
||||
// Token-less attributes with a font-style renderer carry it on `--<fg|effect>-*`, the same
|
||||
// declarations `overrideDeclarations` emits for their overrides.
|
||||
if (!attr?.fontStyle || !base) return colour;
|
||||
const effectColor = `var(${attr.effect ?? attr.fg ?? "--fg-muted"})`;
|
||||
const effect: TextAttributesOverride["effect"] = attr.defaultEffect ?? (attr.effect ? "wave" : undefined);
|
||||
const decoration =
|
||||
effect && effect !== "none"
|
||||
? `var(${attr.effect ?? base}-effect, ${EFFECT_CSS[effect]?.(effectColor) ?? "none"})`
|
||||
: undefined;
|
||||
return {
|
||||
...colour,
|
||||
fontWeight: `var(${base}-bold, ${
|
||||
attr.defaultBold ? "var(--fw-code-bold, 700)" : "inherit"
|
||||
})` as CSSProperties["fontWeight"],
|
||||
fontStyle: `var(${base}-italic, ${attr.defaultItalic ? "italic" : "inherit"})`,
|
||||
...(decoration
|
||||
? { textDecoration: decoration, ...({ WebkitTextDecoration: decoration } as CSSProperties) }
|
||||
// Token-less attributes with a font-style renderer carry it on `--<fg>-*`, the same
|
||||
// declarations `overrideDeclarations` emits for their overrides.
|
||||
...(attr?.fontStyle && (attr.fg ?? attr.effect)
|
||||
? {
|
||||
fontWeight: `var(${attr.fg ?? attr.effect}-bold, inherit)`,
|
||||
fontStyle: `var(${attr.fg ?? attr.effect}-italic, inherit)`,
|
||||
textDecoration: `var(${attr.effect ?? attr.fg}-effect, none)`,
|
||||
}
|
||||
: null),
|
||||
};
|
||||
}
|
||||
if (attr.italicOnly)
|
||||
return { fontStyle: `var(--syn-${attr.token}-italic, ${attr.defaultItalic === false ? "normal" : "italic"})` };
|
||||
const t = attr.token;
|
||||
@@ -221,11 +206,6 @@ const SAMPLES: Record<string, CodeSample> = {
|
||||
[" ", "text"],
|
||||
["legacyCall", "unused"],
|
||||
],
|
||||
[
|
||||
["typo", "typo"],
|
||||
[" ", "text"],
|
||||
["deprecatedCall", "deprecated"],
|
||||
],
|
||||
[
|
||||
["bad\u203dchar", "invalid"],
|
||||
[" \u00b7 ", "whitespaces"],
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-app);
|
||||
outline: none;
|
||||
}
|
||||
.toolbar {
|
||||
flex: none;
|
||||
@@ -85,7 +84,6 @@
|
||||
}
|
||||
.message {
|
||||
flex: 1;
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@@ -541,10 +541,6 @@ export function MergeEditor({ input }: { input: EditorInput }) {
|
||||
}, [actionHost]);
|
||||
|
||||
const focusProps = {
|
||||
// WebKit doesn't focus a clicked button: focus then moves to the nearest focusable
|
||||
// ancestor, which must be this editor for the footer and toolbar buttons to keep it the
|
||||
// focused merge editor their actions run in.
|
||||
tabIndex: -1,
|
||||
onFocusCapture: (e: React.FocusEvent<HTMLDivElement>) => {
|
||||
const target = e.target as Node;
|
||||
if (oursRef.current?.contains(target)) paneRef.current = "left";
|
||||
|
||||
@@ -49,7 +49,6 @@ import { allRepos, runRemote } from "./ops";
|
||||
import { showPushDialog, showUpdateDialog, type PushTarget, type UpdateTarget } from "./remoteUi";
|
||||
import { comparePathsWithRevision, showDirectoryWorkingDiff } from "./comparePaths";
|
||||
import { showCloneDialog } from "./clone";
|
||||
import { createRepository } from "./createRepository";
|
||||
import {
|
||||
showManageRemotes,
|
||||
showMergeDialog,
|
||||
@@ -247,15 +246,6 @@ export function registerVcsActions(): () => void {
|
||||
keyWhen: IN_COMMIT_WINDOW,
|
||||
run: () => useCommit.setState((s) => ({ historyTick: s.historyTick + 1 })),
|
||||
},
|
||||
{
|
||||
id: "vcs.createRepository",
|
||||
title: "Create Git Repository…",
|
||||
category: "Git",
|
||||
icon: "branch",
|
||||
when: "projectOpen && projectLocal",
|
||||
keywords: ["init", "git", "enable version control"],
|
||||
run: () => createRepository(),
|
||||
},
|
||||
{
|
||||
id: "vcs.clone",
|
||||
title: "Get from Version Control…",
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/** VCS › Create Git Repository…: `git init` in a folder chosen under the project. */
|
||||
import { open } from "@/platform/dialog";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { call, commands } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { discoverRoots } from "./store";
|
||||
|
||||
export async function createRepository() {
|
||||
const root = useProject.getState().project?.root;
|
||||
const dir = await open({ title: tx("Select Directory for Git Init"), directory: true, defaultPath: root });
|
||||
if (typeof dir !== "string") return;
|
||||
try {
|
||||
await call(commands.vcsInit(dir));
|
||||
} catch (e) {
|
||||
notifications().notify({
|
||||
group: "vcs",
|
||||
level: "error",
|
||||
title: tx("Couldn't create Git repository"),
|
||||
body: String((e as Error)?.message ?? e),
|
||||
});
|
||||
return;
|
||||
}
|
||||
notifications().notify({ group: "vcs", level: "info", title: tx`Created Git repository in ${dir}` });
|
||||
await discoverRoots();
|
||||
}
|
||||
@@ -266,7 +266,7 @@ export function statusOf(path: string): FileStatus | undefined {
|
||||
}
|
||||
|
||||
/** Repositories found below a project that is not itself under git (scanned in the background). */
|
||||
export async function discoverRoots() {
|
||||
async function discoverRoots() {
|
||||
const root = useVcs.getState().root;
|
||||
if (!root || !isTauri()) return;
|
||||
const roots = await call(commands.vcsRoots()).catch(() => []);
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
ATTRIBUTES,
|
||||
CLEAR_COLOR,
|
||||
_setSchemeSource,
|
||||
overrideDeclarations,
|
||||
resolvedAttribute,
|
||||
schemeAfterThemeChange,
|
||||
schemeCss,
|
||||
@@ -342,43 +341,4 @@ describe("color schemes", () => {
|
||||
expect(v.codeStyle.languages.go).toEqual({ useTab: true });
|
||||
expect(v.general.softWrapFiles).toContain("*.md");
|
||||
});
|
||||
|
||||
it("styles Errors and Warnings attributes beyond colour", () => {
|
||||
const decls = overrideDeclarations({
|
||||
error: { bold: true, italic: true, effect: "strikeout", effectColor: "#ff0000" },
|
||||
unused: { italic: true },
|
||||
});
|
||||
const map = new Map(decls);
|
||||
expect(map.get("--wave-error-bold")).toBe("var(--fw-code-bold, 700)");
|
||||
expect(map.get("--wave-error-italic")).toBe("italic");
|
||||
expect(map.get("--wave-error-effect")).toBe("line-through #ff0000");
|
||||
expect(map.get("--editor-error-decoration")).toBe("line-through #ff0000");
|
||||
expect(map.get("--wave-error")).toBe("#ff0000");
|
||||
expect(map.get("--fg-unused-italic")).toBe("italic");
|
||||
const none = new Map(overrideDeclarations({ warning: { effect: "none" }, deprecated: { effect: "underline" } }));
|
||||
expect(none.get("--editor-warning-decoration")).toBe("none");
|
||||
expect(none.get("--fg-deprecated-effect")).toBe("underline var(--fg-deprecated)");
|
||||
});
|
||||
|
||||
it("keeps the font style of Errors and Warnings in custom schemes", () => {
|
||||
const customs = {
|
||||
"custom-x": {
|
||||
name: "X",
|
||||
parent: "dark" as const,
|
||||
attributes: { error: { bold: true }, deprecated: { effect: "underline" as const } },
|
||||
},
|
||||
};
|
||||
const css = schemeCss("custom-x", undefined, null, customs);
|
||||
expect(css).toContain("--wave-error-bold: var(--fw-code-bold, 700)");
|
||||
expect(css).toContain("--fg-deprecated-effect: underline var(--fg-deprecated)");
|
||||
});
|
||||
|
||||
it("lists typo and deprecated symbol under Errors and Warnings", () => {
|
||||
const ids = ATTRIBUTES.filter((a) => a.path.join("/") === "General/Errors and Warnings").map((a) => a.id);
|
||||
expect(ids).toEqual(["error", "warning", "weakWarning", "typo", "unused", "deprecated", "invalid"]);
|
||||
expect(resolvedAttribute("dark", "typo")).toMatchObject({ effect: "wave" });
|
||||
expect(resolvedAttribute("dark", "deprecated")).toMatchObject({ effect: "strikeout", effectColor: "#8c8f95" });
|
||||
expect(resolvedAttribute("dark", "deprecated").foreground).toBeUndefined();
|
||||
expect(resolvedAttribute("dark", "error")).toMatchObject({ effect: "wave", bold: false, italic: false });
|
||||
});
|
||||
});
|
||||
+17
-70
@@ -31,8 +31,7 @@ export interface AttributeDef {
|
||||
token?: string;
|
||||
/** A semantic modifier whose only configurable attribute is italic. */
|
||||
italicOnly?: boolean;
|
||||
/** The attribute's renderer applies font style and effects (tokens, the Git surfaces,
|
||||
* Errors and Warnings): overrides land on `--<fg|effect>-*`. */
|
||||
/** The attribute's renderer applies font style and effects (tokens, the Git surfaces). */
|
||||
fontStyle?: true;
|
||||
defaultBold?: boolean;
|
||||
defaultItalic?: boolean;
|
||||
@@ -140,46 +139,10 @@ const BASE_ATTRIBUTES: readonly AttributeDef[] = [
|
||||
path: ["General", "Search Results"],
|
||||
bg: "--bg-search-match-current",
|
||||
},
|
||||
{
|
||||
id: "error",
|
||||
name: "Error",
|
||||
path: ["General", "Errors and Warnings"],
|
||||
effect: "--wave-error",
|
||||
defaultEffect: "wave",
|
||||
fontStyle: true,
|
||||
},
|
||||
{
|
||||
id: "warning",
|
||||
name: "Warning",
|
||||
path: ["General", "Errors and Warnings"],
|
||||
effect: "--wave-warning",
|
||||
defaultEffect: "wave",
|
||||
fontStyle: true,
|
||||
},
|
||||
{ id: "error", name: "Error", path: ["General", "Errors and Warnings"], effect: "--wave-error" },
|
||||
{ id: "warning", name: "Warning", path: ["General", "Errors and Warnings"], effect: "--wave-warning" },
|
||||
{ id: "weakWarning", name: "Weak warning", path: ["General", "Errors and Warnings"], fg: "--fg-weak-warning" },
|
||||
{
|
||||
id: "typo",
|
||||
name: "Typo",
|
||||
path: ["General", "Errors and Warnings"],
|
||||
effect: "--wave-typo",
|
||||
defaultEffect: "wave",
|
||||
fontStyle: true,
|
||||
},
|
||||
{
|
||||
id: "unused",
|
||||
name: "Unused symbol",
|
||||
path: ["General", "Errors and Warnings"],
|
||||
fg: "--fg-unused",
|
||||
fontStyle: true,
|
||||
},
|
||||
{
|
||||
id: "deprecated",
|
||||
name: "Deprecated symbol",
|
||||
path: ["General", "Errors and Warnings"],
|
||||
effect: "--fg-deprecated",
|
||||
defaultEffect: "strikeout",
|
||||
fontStyle: true,
|
||||
},
|
||||
{ id: "unused", name: "Unused symbol", path: ["General", "Errors and Warnings"], fg: "--fg-unused" },
|
||||
syn("invalid", "Bad character", ["General", "Errors and Warnings"], "invalid"),
|
||||
syn("todo", "TODO defaults", ["General", "Code"], "todo"),
|
||||
{ id: "inlineValue", name: "Inline debugger values", path: ["Debugger"], fg: "--syn-inline-value" },
|
||||
@@ -376,11 +339,6 @@ export const ATTRIBUTES: readonly AttributeDef[] = [
|
||||
/** Variables a scheme sets: every attribute variable plus the editor tokens derived from them. */
|
||||
const SCHEME_VARS = new Set<string>([
|
||||
...ATTRIBUTES.flatMap((a) => [a.fg, a.bg, a.effect].filter((v): v is string => !!v)),
|
||||
// Font style of token-less attributes (`overrideDeclarations` writes them for custom schemes too).
|
||||
...ATTRIBUTES.flatMap((a) => {
|
||||
const base = a.fg ?? a.effect;
|
||||
return !a.token && a.fontStyle && base ? [`${base}-bold`, `${base}-italic`, `${a.effect ?? base}-effect`] : [];
|
||||
}),
|
||||
"--syn-comment-style",
|
||||
"--syn-doc-comment-tag-effect",
|
||||
"--editor-error-decoration",
|
||||
@@ -519,7 +477,7 @@ export function boldWeight(bold: boolean): string {
|
||||
return bold ? "var(--fw-code-bold, 700)" : "var(--fw-code, 400)";
|
||||
}
|
||||
|
||||
export const EFFECT_CSS: Record<string, (c: string) => string> = {
|
||||
const EFFECT_CSS: Record<string, (c: string) => string> = {
|
||||
underline: (c) => `underline ${c}`,
|
||||
wave: (c) => `underline wavy ${c}`,
|
||||
strikeout: (c) => `line-through ${c}`,
|
||||
@@ -549,12 +507,9 @@ export function overrideDeclarations(
|
||||
}
|
||||
if (setOrCleared(o.background) && attr.bg) out.push([attr.bg, o.background]);
|
||||
if (setOrCleared(o.effectColor) && attr.effect) out.push([attr.effect, o.effectColor]);
|
||||
if (attr.effect && (setOrCleared(o.effectColor) || o.effect)) {
|
||||
const style = o.effect ?? attr.defaultEffect ?? "wave";
|
||||
const color = setOrCleared(o.effectColor) ? o.effectColor! : `var(${attr.effect})`;
|
||||
const decoration = EFFECT_CSS[style]?.(color) ?? "none";
|
||||
if (attr.id === "error") out.push(["--editor-error-decoration", decoration]);
|
||||
if (attr.id === "warning") out.push(["--editor-warning-decoration", decoration]);
|
||||
if (attr.effect && setOrCleared(o.effectColor)) {
|
||||
if (attr.id === "error") out.push(["--editor-error-decoration", `underline wavy ${o.effectColor}`]);
|
||||
if (attr.id === "warning") out.push(["--editor-warning-decoration", `underline wavy ${o.effectColor}`]);
|
||||
}
|
||||
if (attr.token) {
|
||||
const t = attr.token;
|
||||
@@ -579,9 +534,7 @@ export function overrideDeclarations(
|
||||
if (o.effect)
|
||||
out.push([
|
||||
`${attr.effect ?? base}-effect`,
|
||||
EFFECT_CSS[o.effect]?.(
|
||||
setOrCleared(o.effectColor) ? o.effectColor! : attr.effect ? `var(${attr.effect})` : "currentColor",
|
||||
) ?? "none",
|
||||
EFFECT_CSS[o.effect]?.(setOrCleared(o.effectColor) ? o.effectColor! : "currentColor") ?? "none",
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -693,20 +646,14 @@ export function resolvedAttribute(
|
||||
const italicCss =
|
||||
vars.get(`--syn-${token}-italic`) ?? (token === "comment" ? vars.get("--syn-comment-style") : undefined);
|
||||
out.italic = own?.italic ?? italicFromCss(italicCss) ?? attr.defaultItalic ?? false;
|
||||
if (effect) out.effect = effect;
|
||||
} else {
|
||||
// Token-less attributes whose renderer applies font style (the Git segments, Errors and
|
||||
// Warnings) carry their overrides on `--<fg|effect>-*`, the same declarations
|
||||
// `overrideDeclarations` emits.
|
||||
if (attr.fontStyle) {
|
||||
const base = attr.fg ?? attr.effect;
|
||||
const bold = base ? vars.get(`${base}-bold`) : undefined;
|
||||
const italic = base ? vars.get(`${base}-italic`) : undefined;
|
||||
out.bold = boldFromCss(bold) ?? attr.defaultBold ?? false;
|
||||
out.italic = italicFromCss(italic) ?? attr.defaultItalic ?? false;
|
||||
}
|
||||
if (effect) out.effect = effect;
|
||||
}
|
||||
} else if (attr.fontStyle) {
|
||||
const base = attr.fg ?? attr.effect;
|
||||
const bold = base ? vars.get(`${base}-bold`) : undefined;
|
||||
const italic = base ? vars.get(`${base}-italic`) : undefined;
|
||||
if (bold !== undefined) out.bold = boldFromCss(bold) ?? false;
|
||||
if (italic !== undefined) out.italic = italicFromCss(italic) ?? false;
|
||||
} else if (effect) out.effect = effect;
|
||||
if (token && effect) out.effect = effect;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -1510,9 +1510,5 @@
|
||||
"{name} (group)": "{name} (group)",
|
||||
"Servers in group": "Servers in group",
|
||||
"Upload to… on this group uploads the files to each of its servers in turn.": "Upload to… on this group uploads the files to each of its servers in turn.",
|
||||
"Server Group": "Server Group",
|
||||
"Create Git Repository…": "Create Git Repository…",
|
||||
"Select Directory for Git Init": "Select Directory for Git Init",
|
||||
"Couldn't create Git repository": "Couldn't create Git repository",
|
||||
"Created Git repository in {dir}": "Created Git repository in {dir}"
|
||||
"Server Group": "Server Group"
|
||||
}
|
||||
@@ -7911,9 +7911,5 @@
|
||||
"{name} (group)": "{name}(分组)",
|
||||
"Servers in group": "组内服务器",
|
||||
"Upload to… on this group uploads the files to each of its servers in turn.": "对此分组执行“上传到…”时,会依次上传到组内的每台服务器。",
|
||||
"Server Group": "服务器分组",
|
||||
"Create Git Repository…": "创建 Git 仓库…",
|
||||
"Select Directory for Git Init": "选择要执行 Git Init 的目录",
|
||||
"Couldn't create Git repository": "无法创建 Git 仓库",
|
||||
"Created Git repository in {dir}": "已在 {dir} 创建 Git 仓库"
|
||||
"Server Group": "服务器分组"
|
||||
}
|
||||
@@ -73,11 +73,6 @@ export function invalidateVirtualDocument(uri: string): void {
|
||||
cache.delete(uri);
|
||||
}
|
||||
|
||||
/** Forgets every cached document: a build tool reload may have replaced library jars in place. */
|
||||
export function invalidateVirtualDocuments(): void {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
/** Contents of a virtual document (cached; failures are retried on the next call). */
|
||||
export function virtualDocument(uri: string): Promise<VirtualDocument> {
|
||||
let p = cache.get(uri);
|
||||
|
||||
@@ -204,8 +204,6 @@ html {
|
||||
--fg-fold-marker: #6f737a;
|
||||
--fg-whitespace: #6f737a;
|
||||
--fg-unused: #6f737a;
|
||||
--wave-typo: #59a869;
|
||||
--fg-deprecated: #8c8f95;
|
||||
--bg-identifier-under-caret: #373b39;
|
||||
--bg-identifier-under-caret-write: #402f33;
|
||||
--fg-inlay: #868a91;
|
||||
@@ -645,8 +643,6 @@ html {
|
||||
--fg-fold-marker: #8b8e94;
|
||||
--fg-whitespace: #b5b7bd;
|
||||
--fg-unused: #8c8c8c;
|
||||
--wave-typo: #3d9950;
|
||||
--fg-deprecated: #6c707e;
|
||||
--bg-identifier-under-caret: #edebfc;
|
||||
--bg-identifier-under-caret-write: #fce8f4;
|
||||
--fg-inlay: #73767c;
|
||||
@@ -939,8 +935,6 @@ html {
|
||||
--fg-fold-marker: #9fa2a9;
|
||||
--fg-whitespace: #8b8e95;
|
||||
--fg-unused: #8b8e95;
|
||||
--wave-typo: #6cbb7d;
|
||||
--fg-deprecated: #b6b8be;
|
||||
--bg-identifier-under-caret: #3f4a44;
|
||||
--bg-identifier-under-caret-write: #553840;
|
||||
--fg-inlay: #ccced3;
|
||||
@@ -1153,8 +1147,6 @@ html {
|
||||
--fg-fold-marker: var(--palette-muted);
|
||||
--fg-whitespace: var(--palette-muted);
|
||||
--fg-unused: var(--palette-muted);
|
||||
--wave-typo: var(--palette-green);
|
||||
--fg-deprecated: var(--palette-muted);
|
||||
--fg-inlay: var(--palette-muted);
|
||||
--fg-weak-warning: var(--palette-muted);
|
||||
--syn-inline-value: var(--palette-muted);
|
||||
|
||||
@@ -344,9 +344,6 @@ export function MenuList({
|
||||
aria-label={aria["aria-label"]}
|
||||
className={styles.menu}
|
||||
onKeyDown={onKeyDown}
|
||||
// A right-click in an open menu does nothing; without this it reaches whatever is under the
|
||||
// menu in the component tree, e.g. the title bar's toolbar menu behind File › Open Recent.
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onPointerMove={(e) => {
|
||||
if (e.currentTarget.contains(e.target as Node)) pointerMove({ x: e.clientX, y: e.clientY });
|
||||
}}
|
||||
|
||||
@@ -283,17 +283,6 @@ describe("Menu action enablement", () => {
|
||||
expect(open).toHaveFocus();
|
||||
});
|
||||
|
||||
it("keeps a right-click in a menu from reaching the components behind it", () => {
|
||||
const behind = vi.fn((e: { defaultPrevented: boolean }) => e.defaultPrevented);
|
||||
render(
|
||||
<div onContextMenu={(e) => behind(e)}>
|
||||
<MenuList items={[{ label: "Recent", run: () => undefined }]} onDone={() => {}} />
|
||||
</div>,
|
||||
);
|
||||
fireEvent.contextMenu(screen.getByRole("menuitem", { name: "Recent" }));
|
||||
expect(behind).toHaveReturnedWith(true);
|
||||
});
|
||||
|
||||
it("reports a hovered row once and lets the keyboard continue from it", () => {
|
||||
const active: string[] = [];
|
||||
const item = (label: string) => ({ label, onActive: () => active.push(label), run: () => undefined });
|
||||
|
||||
@@ -95,7 +95,7 @@ function ProjectButton() {
|
||||
);
|
||||
}
|
||||
|
||||
const VCS_FALLBACK_ACTIONS = ["vcs.createRepository", "vcs.clone", "vcs.localHistory", "vcs.showLog"];
|
||||
const VCS_FALLBACK_ACTIONS = ["vcs.clone", "vcs.localHistory", "vcs.showLog"];
|
||||
|
||||
/** "Version Control ▾" shown while no VCS widget is contributed (project without a repository). */
|
||||
function VcsFallback() {
|
||||
|
||||
Reference in new issue
Block a user