From 4dcf7b0fb285f45d088fdf02940031e436ab9d39 Mon Sep 17 00:00:00 2001 From: Shohruh Ismoilov Date: Sat, 7 Nov 2026 22:58:31 +0500 Subject: [PATCH] fix(build): compile all Go workspace modules (#1992) Build in a go.work project compiled only one module. It now builds every workspace member, including members outside the project folder, with GOWORK pointing at the canonical go.work so external members keep their workspace context. Co-authored-by: shohruh-genius <7658451+shohruh-genius@users.noreply.github.com> --- crates/ide-build/src/compile/mod.rs | 92 ++++++++++++++++++++- crates/ide-core/src/go.rs | 124 ++++++++++++++++++++++++++++ crates/ide-core/src/lib.rs | 1 + crates/ide-run/src/go.rs | 122 +-------------------------- 4 files changed, 215 insertions(+), 124 deletions(-) create mode 100644 crates/ide-core/src/go.rs diff --git a/crates/ide-build/src/compile/mod.rs b/crates/ide-build/src/compile/mod.rs index ce41a1089..8b4119225 100644 --- a/crates/ide-build/src/compile/mod.rs +++ b/crates/ide-build/src/compile/mod.rs @@ -153,7 +153,7 @@ fn detect_dir(dir: &Path, model: &BuildModel, root: &Path) -> Option gradle_steps(root, model, kind, module_dir.as_deref())?, BuildSystem::Cargo => cargo_steps(&dir, kind), // Go builds the file's package (a module holds many). - BuildSystem::Go => go_steps(file.and_then(Path::parent).unwrap_or(&dir), kind), + BuildSystem::Go => go_steps(file.and_then(Path::parent).unwrap_or(&dir), kind)?, BuildSystem::TypeScript => ts_steps(&dir, kind), BuildSystem::CMake => cmake_steps(&dir, kind, cmake.and_then(<[_]>::first)), }; @@ -374,7 +374,31 @@ fn cmake_steps(dir: &Path, kind: BuildKind, profile: Option<&CMakeProfile>) -> V steps } -fn go_steps(dir: &Path, kind: BuildKind) -> Vec { +fn go_steps(dir: &Path, kind: BuildKind) -> IdeResult> { + if dir.join("go.work").is_file() { + let work_file = dunce::canonicalize(dir.join("go.work"))?; + let modules = ide_core::go::work_modules(&std::fs::read_to_string(dir.join("go.work"))?); + if modules.is_empty() { + return Err(IdeError::invalid("The Go workspace contains no modules")); + } + let mut steps = Vec::new(); + for module in modules { + let module = dir.join(module); + if !module.join("go.mod").is_file() { + return Err(IdeError::not_found(format!("Go workspace module has no go.mod: {}", module.display()))); + } + let mut module_steps = go_module_steps(&module, kind); + for step in &mut module_steps { + step.env.insert("GOWORK".into(), work_file.to_string_lossy().into_owned()); + } + steps.extend(module_steps); + } + return Ok(steps); + } + Ok(go_module_steps(dir, kind)) +} + +fn go_module_steps(dir: &Path, kind: BuildKind) -> Vec { let module_root = dir.ancestors().find(|d| d.join("go.mod").is_file()).unwrap_or(dir); let target = if module_root == dir { "./...".to_owned() } else { format!("./{}/...", rel_str(module_root, dir)) }; let mut args = vec!["build"]; @@ -813,6 +837,68 @@ fn launch_class_dirs(system: BuildSystem, module: &Path, test: bool) -> Vec Vec { + let mut tokens = Vec::new(); + let mut chars = text.chars().peekable(); + while let Some(ch) = chars.next() { + match ch { + '\n' | '(' | ')' => tokens.push(ch.to_string()), + ch if ch.is_whitespace() => {} + '/' if chars.peek() == Some(&'/') => { + for c in chars.by_ref() { + if c == '\n' { + tokens.push("\n".into()); + break; + } + } + } + '"' | '`' => { + let Some(token) = quoted_work_path(&mut chars, ch) else { return Vec::new() }; + tokens.push(token); + } + _ => { + let mut token = ch.to_string(); + while chars.peek().is_some_and(|c| !c.is_whitespace() && *c != '(' && *c != ')') { + token.push(chars.next().unwrap()); + } + tokens.push(token); + } + } + } + let mut out = Vec::new(); + let mut start = true; + let mut tokens = tokens.into_iter(); + while let Some(token) = tokens.next() { + if token == "\n" { + start = true; + continue; + } + if start + && token == "use" + && let Some(path) = tokens.next() + { + if path == "(" { + let mut paths = Vec::new(); + let mut closed = false; + for path in tokens.by_ref() { + if path == ")" { + closed = true; + break; + } + if path != "\n" && !path.is_empty() { + paths.push(PathBuf::from(path)); + } + } + if closed { + out.extend(paths); + } + } else if path != "\n" && !path.is_empty() { + out.push(PathBuf::from(path)); + } + } + start = false; + } + let mut seen = std::collections::HashSet::new(); + out.retain(|path| seen.insert(path.clone())); + out +} + +fn quoted_work_path(chars: &mut std::iter::Peekable>, quote: char) -> Option { + let mut bytes = Vec::new(); + while let Some(ch) = chars.next() { + if ch == quote { + return String::from_utf8(bytes).ok(); + } + if ch == '\\' && quote == '"' { + let escaped = chars.next()?; + let byte = match escaped { + 'a' => 7, + 'b' => 8, + 'f' => 12, + 'n' => 10, + 'r' => 13, + 't' => 9, + 'v' => 11, + '\\' => b'\\', + '"' => b'"', + 'x' => { + let digits: String = chars.by_ref().take(2).collect(); + if digits.len() != 2 { + return None; + } + u8::from_str_radix(&digits, 16).ok()? + } + 'u' | 'U' => { + let width = if escaped == 'u' { 4 } else { 8 }; + let digits: String = chars.by_ref().take(width).collect(); + if digits.len() != width { + return None; + } + let ch = char::from_u32(u32::from_str_radix(&digits, 16).ok()?)?; + bytes.extend_from_slice(ch.to_string().as_bytes()); + continue; + } + '0'..='7' => { + let digits = format!("{escaped}{}", chars.by_ref().take(2).collect::()); + if digits.len() != 3 { + return None; + } + u8::from_str_radix(&digits, 8).ok()? + } + _ => return None, + }; + bytes.push(byte); + } else { + if ch == '\n' && quote == '"' { + return None; + } + bytes.extend_from_slice(ch.to_string().as_bytes()); + } + } + None +} diff --git a/crates/ide-core/src/lib.rs b/crates/ide-core/src/lib.rs index aa7db963a..6234af032 100644 --- a/crates/ide-core/src/lib.rs +++ b/crates/ide-core/src/lib.rs @@ -7,6 +7,7 @@ pub mod editor_settings; pub mod error; pub mod event; +pub mod go; pub mod ids; pub mod ignored; pub mod inspection_settings; diff --git a/crates/ide-run/src/go.rs b/crates/ide-run/src/go.rs index 18e8225bb..5593b46e0 100644 --- a/crates/ide-run/src/go.rs +++ b/crates/ide-run/src/go.rs @@ -269,127 +269,7 @@ pub fn prepare_test_file(root: &Path, config: &mut RunConfig) -> IdeResult<()> { Ok(()) } -/// Module directories from a go.work file. Quoted paths and comments follow Go's file syntax. -pub fn work_modules(text: &str) -> Vec { - let mut tokens = Vec::new(); - let mut chars = text.chars().peekable(); - while let Some(ch) = chars.next() { - match ch { - '\n' | '(' | ')' => tokens.push(ch.to_string()), - ch if ch.is_whitespace() => {} - '/' if chars.peek() == Some(&'/') => { - for c in chars.by_ref() { - if c == '\n' { - tokens.push("\n".into()); - break; - } - } - } - '"' | '`' => { - let Some(token) = quoted_work_path(&mut chars, ch) else { return Vec::new() }; - tokens.push(token); - } - _ => { - let mut token = ch.to_string(); - while chars.peek().is_some_and(|c| !c.is_whitespace() && *c != '(' && *c != ')') { - token.push(chars.next().unwrap()); - } - tokens.push(token); - } - } - } - let mut out = Vec::new(); - let mut start = true; - let mut tokens = tokens.into_iter(); - while let Some(token) = tokens.next() { - if token == "\n" { - start = true; - continue; - } - if start - && token == "use" - && let Some(path) = tokens.next() - { - if path == "(" { - let mut paths = Vec::new(); - let mut closed = false; - for path in tokens.by_ref() { - if path == ")" { - closed = true; - break; - } - if path != "\n" && !path.is_empty() { - paths.push(PathBuf::from(path)); - } - } - if closed { - out.extend(paths); - } - } else if path != "\n" && !path.is_empty() { - out.push(PathBuf::from(path)); - } - } - start = false; - } - let mut seen = std::collections::HashSet::new(); - out.retain(|path| seen.insert(path.clone())); - out -} - -fn quoted_work_path(chars: &mut std::iter::Peekable>, quote: char) -> Option { - let mut bytes = Vec::new(); - while let Some(ch) = chars.next() { - if ch == quote { - return String::from_utf8(bytes).ok(); - } - if ch == '\\' && quote == '"' { - let escaped = chars.next()?; - let byte = match escaped { - 'a' => 7, - 'b' => 8, - 'f' => 12, - 'n' => 10, - 'r' => 13, - 't' => 9, - 'v' => 11, - '\\' => b'\\', - '"' => b'"', - 'x' => { - let digits: String = chars.by_ref().take(2).collect(); - if digits.len() != 2 { - return None; - } - u8::from_str_radix(&digits, 16).ok()? - } - 'u' | 'U' => { - let width = if escaped == 'u' { 4 } else { 8 }; - let digits: String = chars.by_ref().take(width).collect(); - if digits.len() != width { - return None; - } - let ch = char::from_u32(u32::from_str_radix(&digits, 16).ok()?)?; - bytes.extend_from_slice(ch.to_string().as_bytes()); - continue; - } - '0'..='7' => { - let digits = format!("{escaped}{}", chars.by_ref().take(2).collect::()); - if digits.len() != 3 { - return None; - } - u8::from_str_radix(&digits, 8).ok()? - } - _ => return None, - }; - bytes.push(byte); - } else { - if ch == '\n' && quote == '"' { - return None; - } - bytes.extend_from_slice(ch.to_string().as_bytes()); - } - } - None -} +pub use ide_core::go::work_modules; #[cfg(test)] mod test_scope_tests {