@@ -55,6 +55,30 @@ const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): Roo
5555 }
5656}
5757
58+ const StrictNearestRoot = ( includePatterns : string [ ] , excludePatterns ?: string [ ] ) : RootFunction => {
59+ return async ( file , ctx ) => {
60+ if ( excludePatterns ) {
61+ const excludedFiles = Filesystem . up ( {
62+ targets : excludePatterns ,
63+ start : path . dirname ( file ) ,
64+ stop : ctx . directory ,
65+ } )
66+ const excluded = await excludedFiles . next ( )
67+ await excludedFiles . return ( )
68+ if ( excluded . value ) return undefined
69+ }
70+ const files = Filesystem . up ( {
71+ targets : includePatterns ,
72+ start : path . dirname ( file ) ,
73+ stop : ctx . directory ,
74+ } )
75+ const first = await files . next ( )
76+ await files . return ( )
77+ if ( ! first . value ) return undefined
78+ return path . dirname ( first . value )
79+ }
80+ }
81+
5882export interface Info {
5983 id : string
6084 extensions : string [ ]
@@ -1173,31 +1197,63 @@ export const Astro: Info = {
11731197 } ,
11741198}
11751199
1200+ function isModuleOf ( pomContent : string , modulePath : string ) : boolean {
1201+ const normalized = modulePath . replace ( / \\ / g, "/" ) . replace ( / \/ $ / , "" )
1202+ if ( ! normalized ) return false
1203+ const modulesBlocks = pomContent . match ( / < m o d u l e s > ( [ \s \S ] * ?) < \/ m o d u l e s > / g) ?? [ ]
1204+ for ( const block of modulesBlocks ) {
1205+ const stripped = block . replace ( / < ! - - [ \s \S ] * ?- - > / g, "" )
1206+ for ( const m of stripped . matchAll ( / < m o d u l e > \s * ( [ ^ < ] + ?) \s * < \/ m o d u l e > / g) ) {
1207+ const decl = m [ 1 ] . replace ( / \\ / g, "/" ) . replace ( / ^ \. \/ / , "" ) . replace ( / \/ $ / , "" )
1208+ if ( decl === normalized ) return true
1209+ }
1210+ }
1211+ return false
1212+ }
1213+
11761214export const JDTLS : Info = {
11771215 id : "jdtls" ,
11781216 root : async ( file , ctx ) => {
1179- // Without exclusions, NearestRoot defaults to instance directory so we can't
1180- // distinguish between a) no project found and b) project found at instance dir.
1181- // So we can't choose the root from (potential) monorepo markers first.
1182- // Look for potential subproject markers first while excluding potential monorepo markers.
11831217 const settingsMarkers = [ "settings.gradle" , "settings.gradle.kts" ]
11841218 const gradleMarkers = [ "gradlew" , "gradlew.bat" ]
1185- const exclusionsForMonorepos = gradleMarkers . concat ( settingsMarkers )
1186-
1187- const [ projectRoot , wrapperRoot , settingsRoot ] = await Promise . all ( [
1188- NearestRoot ( [ "pom.xml" , "build.gradle" , "build.gradle.kts" , ".project" , ".classpath" ] , exclusionsForMonorepos ) (
1189- file ,
1190- ctx ,
1191- ) ,
1192- NearestRoot ( gradleMarkers , settingsMarkers ) ( file , ctx ) ,
1193- NearestRoot ( settingsMarkers ) ( file , ctx ) ,
1219+ // 1. Gradle (unchanged from original logic)
1220+ const [ wrapperRoot , settingsRoot ] = await Promise . all ( [
1221+ StrictNearestRoot ( gradleMarkers , settingsMarkers ) ( file , ctx ) ,
1222+ StrictNearestRoot ( settingsMarkers ) ( file , ctx ) ,
11941223 ] )
1195-
1196- // If projectRoot is undefined we know we are in a monorepo or no project at all.
1197- // So can safely fall through to the other roots
1198- if ( projectRoot ) return projectRoot
11991224 if ( wrapperRoot ) return wrapperRoot
12001225 if ( settingsRoot ) return settingsRoot
1226+
1227+ // 2. Gradle single-project fallback (build.gradle without settings.gradle)
1228+ const buildRoot = await StrictNearestRoot ( [ "build.gradle" , "build.gradle.kts" ] ) ( file , ctx )
1229+ if ( buildRoot ) return buildRoot
1230+
1231+ // 3. Maven: walk up pom.xml chain verifying <module> relationships
1232+ const pomFiles = await Filesystem . findUp (
1233+ "pom.xml" ,
1234+ path . dirname ( file ) ,
1235+ ctx . directory ,
1236+ )
1237+ if ( pomFiles . length > 0 ) {
1238+ let root = path . dirname ( pomFiles [ 0 ] )
1239+ for ( let i = 1 ; i < pomFiles . length ; i ++ ) {
1240+ const parentDir = path . dirname ( pomFiles [ i ] )
1241+ const rel = path . relative ( parentDir , root )
1242+ const content = await fs . readFile ( pomFiles [ i ] , "utf-8" ) . catch ( ( ) => null )
1243+ if ( content && isModuleOf ( content , rel ) ) {
1244+ root = parentDir
1245+ } else {
1246+ break
1247+ }
1248+ }
1249+ return root
1250+ }
1251+
1252+ // 4. Eclipse native project fallback
1253+ const eclipseRoot = await StrictNearestRoot ( [ ".project" , ".classpath" ] ) ( file , ctx )
1254+ if ( eclipseRoot ) return eclipseRoot
1255+
1256+ return undefined
12011257 } ,
12021258 extensions : [ ".java" ] ,
12031259 async spawn ( root , _ctx , flags ) {
0 commit comments