diff --git a/crates/ide-host/src/ops/jvm_icons.rs b/crates/ide-host/src/ops/jvm_icons.rs index a9d24773..ca627784 100644 --- a/crates/ide-host/src/ops/jvm_icons.rs +++ b/crates/ide-host/src/ops/jvm_icons.rs @@ -1,7 +1,7 @@ //! Bounded, read-only JVM presentation for visible files, also served remotely. use crate::Host; use ide_core::{CancellationToken, IdeError, IdeResult, LanguageId}; -use ide_syntax::{JvmPresentation, Snapshot}; +use ide_syntax::{JvmPresentation, Snapshot, StructureService, jvm_type_header}; use serde::{Deserialize, Serialize}; use specta::Type; use std::{ @@ -38,6 +38,27 @@ fn prefix(path: &PathBuf) -> std::io::Result { Ok(String::from_utf8_lossy(&bytes).into_owned()) } +/// The presentation of the file at `path` from its first [`PREFIX_BYTES`]. +fn read_presentation( + structure: &StructureService, + path: &PathBuf, + language: &LanguageId, +) -> IdeResult { + let source = prefix(path)?; + let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or_default(); + let present = + |source: &str| structure.jvm_presentation(&Snapshot::text(language, source), stem, &CancellationToken::new()); + let presentation = present(&source)?; + // Cut off inside its body, a large file doesn't parse as a declaration: its header does. + if presentation.kind.is_none() + && source.len() as u64 >= PREFIX_BYTES + && let Some(header) = jvm_type_header(&source) + { + return Ok(present(&format!("{header}}}"))?); + } + Ok(presentation) +} + pub async fn jvm_file_presentations( host: &Host, window: &str, @@ -67,13 +88,7 @@ pub async fn jvm_file_presentations( let presentation = if let Some(hit) = hit { hit.presentation } else { - let Ok(source) = prefix(&path) else { continue }; - let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or_default(); - let presentation = project.structure.jvm_presentation( - &Snapshot::text(&language, &source), - stem, - &CancellationToken::new(), - )?; + let Ok(presentation) = read_presentation(&project.structure, &path, &language) else { continue }; let mut cache = cache.lock().unwrap(); if cache.len() >= MAX_CACHE { cache.clear(); @@ -99,4 +114,21 @@ mod tests { assert_eq!(prefix(&path).unwrap().len(), PREFIX_BYTES as usize); std::fs::remove_file(path).unwrap(); } + + #[test] + fn large_files_cut_off_in_their_body_keep_their_kind() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("Foo.java"); + let mut text = String::from("package a;\n\n@Service\npublic class Foo implements IFoo {\n"); + while text.len() < PREFIX_BYTES as usize + 1024 { + // The prefix ends inside one of these comments. + text.push_str(" /**\n * Does something.\n */\n public void m() { call(\"x\"); }\n"); + } + std::fs::write(&path, &text).unwrap(); + let syntax = std::sync::Arc::new(ide_language::Syntax::new()); + let structure = StructureService::new(std::sync::Arc::new(ide_language::SyntaxCache::new(syntax))); + let java = LanguageId::new("java"); + let presentation = read_presentation(&structure, &path, &java).unwrap(); + assert_eq!(presentation.kind, Some(ide_syntax::JvmTypeKind::Class)); + } } diff --git a/crates/ide-syntax/src/jvm.rs b/crates/ide-syntax/src/jvm.rs index 88e3c7ca..594991c1 100644 --- a/crates/ide-syntax/src/jvm.rs +++ b/crates/ide-syntax/src/jvm.rs @@ -101,6 +101,44 @@ fn members(node: Node<'_>) -> Vec> { out } +/// The source up to and including the opening brace of the first type's body, skipping +/// comments, string and character literals and parenthesised arguments (annotations, Kotlin +/// constructors). A file cut off somewhere in its body does not parse as a declaration; this +/// header, closed with `}`, still tells what it declares. +pub fn jvm_type_header(source: &str) -> Option<&str> { + let b = source.as_bytes(); + let find = |from: usize, needle: &str| source.get(from..)?.find(needle).map(|i| from + i); + let (mut i, mut parens, mut seen_type) = (0, 0usize, false); + while i < b.len() { + match b[i] { + b'/' if b.get(i + 1) == Some(&b'/') => i = find(i, "\n")?, + b'/' if b.get(i + 1) == Some(&b'*') => i = find(i + 2, "*/")? + 1, + b'"' if source[i..].starts_with("\"\"\"") => i = find(i + 3, "\"\"\"")? + 2, + q @ (b'"' | b'\'') => { + i += 1; + while i < b.len() && b[i] != q && b[i] != b'\n' { + i += if b[i] == b'\\' { 2 } else { 1 }; + } + } + b'(' => parens += 1, + b')' => parens = parens.saturating_sub(1), + b'{' if parens == 0 && seen_type => return Some(&source[..=i]), + c if c.is_ascii_alphabetic() || c == b'_' => { + let start = i; + while i + 1 < b.len() && (b[i + 1].is_ascii_alphanumeric() || matches!(b[i + 1], b'_' | b'$')) { + i += 1; + } + let word = &source[start..=i]; + let declares = matches!(word, "class" | "interface" | "enum" | "record" | "object"); + seen_type |= parens == 0 && declares && (start == 0 || b[start - 1] != b'.'); + } + _ => {} + } + i += 1; + } + None +} + fn is_type(node: Node) -> bool { matches!( node.kind(), diff --git a/crates/ide-syntax/src/lib.rs b/crates/ide-syntax/src/lib.rs index b7238288..34dc123f 100644 --- a/crates/ide-syntax/src/lib.rs +++ b/crates/ide-syntax/src/lib.rs @@ -29,7 +29,7 @@ mod types; #[cfg(test)] mod tests; -pub use jvm::{JvmPresentation, JvmTypeKind}; +pub use jvm::{JvmPresentation, JvmTypeKind, jvm_type_header}; pub use types::*; use ide_core::{CancellationToken, DocId, IdeError, LanguageId, Pos, Range}; diff --git a/crates/ide-syntax/src/tests/jvm.rs b/crates/ide-syntax/src/tests/jvm.rs index fb95ef15..bf051004 100644 --- a/crates/ide-syntax/src/tests/jvm.rs +++ b/crates/ide-syntax/src/tests/jvm.rs @@ -1,5 +1,5 @@ use super::*; -use crate::{JvmPresentation, JvmTypeKind::*}; +use crate::{JvmPresentation, JvmTypeKind::*, jvm_type_header}; fn presentation(language: &str, text: &str) -> JvmPresentation { service().jvm_presentation(&Snapshot::text(&lang(language), text), "Foo", &none()).unwrap() @@ -76,3 +76,30 @@ fn runnable_methods() { .runnable ); } + +#[test] +fn a_cut_off_file_is_classified_by_its_type_header() { + for (source, header) in [ + ( + "package a.record;\n/* class { */\n@Ann({\"}\"}) // interface {\npublic class Foo implements IFoo {\n void m() {", + "package a.record;\n/* class { */\n@Ann({\"}\"}) // interface {\npublic class Foo implements IFoo {", + ), + ("@interface Foo { String value() default \"{\";", "@interface Foo {"), + ( + "class Foo(val a: Map Unit> = mapOf()) : Base() {\n fun f() {", + "class Foo(val a: Map Unit> = mapOf()) : Base() {", + ), + ( + "record Foo(int x) implements Comparable {\n static String S = \"\"\"\n {", + "record Foo(int x) implements Comparable {", + ), + ] { + assert_eq!(jvm_type_header(source), Some(header), "{source}"); + } + assert_eq!(jvm_type_header("package a;\nimport b.C;\n/* unterminated class {"), None); + + let cut = "@Service\npublic abstract class Foo extends Base {\n void m() {\n /*\n * doc"; + assert_eq!(presentation("java", cut).kind, None, "the cut-off text itself doesn't parse"); + let closed = format!("{}}}", jvm_type_header(cut).unwrap()); + assert_eq!(presentation("java", &closed).kind, Some(AbstractClass)); +}