diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index c1dadf8..7e42feb 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -4,6 +4,7 @@ {"id":"zed-css-variables-3iu","title":"Bump css-variable-lsp beta","description":"Update extension to use css-variable-lsp@1.0.8-beta.1","status":"closed","priority":1,"issue_type":"chore","created_at":"2026-01-03T20:17:44.563976+02:00","created_by":"applesucks","updated_at":"2026-01-03T20:18:36.583379+02:00","closed_at":"2026-01-03T20:18:36.583379+02:00","close_reason":"Updated extension to use css-variable-lsp@1.0.8-beta.1"} {"id":"zed-css-variables-3wa","title":"Rename language server id to css-variables","description":"Switch language server id from css_variables to css-variables in manifest, code, and docs.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-05T20:34:51.288227+02:00","created_by":"applesucks","updated_at":"2026-01-05T20:35:55.070606+02:00","closed_at":"2026-01-05T20:35:55.070606+02:00","close_reason":"Completed"} {"id":"zed-css-variables-9w7","title":"Update CI version check to 0.0.5","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-03T22:44:36.964865+02:00","created_by":"applesucks","updated_at":"2026-01-03T22:45:57.438497+02:00","closed_at":"2026-01-03T22:45:57.438497+02:00","close_reason":"Closed"} +{"id":"zed-css-variables-ap3","title":"Fix PATH offline fallback and semver cache ordering regressions","notes":"Linked Linear issue: LMN-13","status":"closed","priority":2,"issue_type":"task","owner":"putaluta@tuta.io","created_at":"2026-02-19T13:23:37.04256+02:00","created_by":"lmn451","updated_at":"2026-02-19T13:26:22.788651+02:00","closed_at":"2026-02-19T13:26:22.788651+02:00","close_reason":"Implemented PATH offline fallback handling and semantic-version cache ordering"} {"id":"zed-css-variables-ate","title":"Sync AGENTS.md with bd onboard snippet","description":"Update AGENTS.md to match latest bd onboard guidance.","status":"closed","priority":3,"issue_type":"chore","created_at":"2026-01-03T18:30:56.462473+02:00","created_by":"applesucks","updated_at":"2026-01-03T18:31:17.337014+02:00","closed_at":"2026-01-03T18:31:17.337014+02:00","close_reason":"Updated AGENTS.md per bd onboard.","dependencies":[{"issue_id":"zed-css-variables-ate","depends_on_id":"zed-css-variables-vh5","type":"discovered-from","created_at":"2026-01-03T18:30:56.463708+02:00","created_by":"applesucks"}]} {"id":"zed-css-variables-cid","title":"Update css-variable-lsp to version 1.0.5-beta.1","description":"Updated the extension to use css-variable-lsp@1.0.5-beta.1 instead of 1.0.2. Changes made to src/lib.rs and package.json.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-30T21:49:42.838797+02:00","updated_at":"2025-12-30T22:02:20.803736+02:00","closed_at":"2025-12-30T22:02:20.803736+02:00"} {"id":"zed-css-variables-dad","title":"Update docs and changelog for 0.0.5","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-03T22:39:34.009633+02:00","created_by":"applesucks","updated_at":"2026-01-03T22:42:24.371458+02:00","closed_at":"2026-01-03T22:42:24.371458+02:00","close_reason":"Closed"} @@ -11,6 +12,7 @@ {"id":"zed-css-variables-f2e","title":"Update CI workflow","description":"Run extension tests on master using GitHub Actions","status":"closed","priority":2,"issue_type":"chore","created_at":"2026-01-03T21:15:53.697057+02:00","created_by":"applesucks","updated_at":"2026-01-03T21:17:09.51316+02:00","closed_at":"2026-01-03T21:17:09.51316+02:00","close_reason":"Updated GitHub Actions to run tests on master and include clean install"} {"id":"zed-css-variables-foa","title":"Add framework examples and commit artifacts","description":"Add Vue/Svelte/Astro/Ripple examples in example/ and commit existing AGENTS.md + extension.wasm updates","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-03T18:49:36.851219+02:00","created_by":"applesucks","updated_at":"2026-01-03T18:50:32.419325+02:00","closed_at":"2026-01-03T18:50:32.419325+02:00","close_reason":"Added Vue/Svelte/Astro/Ripple examples and committed existing artifacts"} {"id":"zed-css-variables-j5t","title":"Default ignore globs should include /** to avoid scanning vendor dirs","status":"closed","priority":2,"issue_type":"bug","created_at":"2026-01-06T18:48:54.159425+02:00","created_by":"applesucks","updated_at":"2026-01-06T22:00:54.426342+02:00","closed_at":"2026-01-06T22:00:54.426342+02:00","close_reason":"Done"} +{"id":"zed-css-variables-qbw","title":"Always-up-to-date PATH/npm fallback for css-variable-lsp resolution","notes":"Linked Linear issue: LMN-13","status":"closed","priority":2,"issue_type":"task","owner":"putaluta@tuta.io","created_at":"2026-02-19T12:51:04.178429+02:00","created_by":"lmn451","updated_at":"2026-02-19T12:56:55.551284+02:00","closed_at":"2026-02-19T12:56:55.551284+02:00","close_reason":"Implemented dynamic latest rust resolution, PATH version gating, and npm fallback update flow"} {"id":"zed-css-variables-qye","title":"Investigate whether completion UI can be styled with CSS","description":"Determine if Zed completion window supports CSS styling or is limited to theme settings.","status":"closed","priority":3,"issue_type":"task","assignee":"applesucks","created_at":"2026-01-03T18:39:02.500207+02:00","created_by":"applesucks","updated_at":"2026-01-03T18:39:34.119615+02:00","closed_at":"2026-01-03T18:39:34.119615+02:00","close_reason":"Zed completion UI is not CSS-styleable; only theme + font settings apply."} {"id":"zed-css-variables-ult","title":"Allow local Rust LSP binary override","description":"Add a dev override to point the extension at a local css-variable-lsp binary for testing, and rebuild extension.wasm.","status":"closed","priority":2,"issue_type":"task","owner":"putaluta@tuta.io","created_at":"2026-02-01T18:42:44.245131+02:00","created_by":"lmn451","updated_at":"2026-02-01T20:02:53.815069+02:00","closed_at":"2026-02-01T20:02:53.815069+02:00","close_reason":"Added jsLspPath override for local TS LSP"} {"id":"zed-css-variables-vdr","title":"Add command-line flags to control color box display","description":"Implement --no-color-preview and --color-only-variables flags (and CSS_LSP_COLOR_ONLY_VARIABLES env var) to control when color decorations are shown. This allows users to show color boxes only on variable definitions, not on usages.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-01-01T14:20:42.664382+02:00","updated_at":"2026-01-01T14:22:40.047257+02:00","closed_at":"2026-01-01T14:22:40.047257+02:00"} diff --git a/src/lib.rs b/src/lib.rs index 3200e3a..8445993 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,39 @@ use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; use zed::serde_json::Value; use zed::settings::{CommandSettings, LspSettings}; use zed_extension_api as zed; const CSS_VARIABLES_BINARY_NAME: &str = "css-variable-lsp"; +const CSS_VARIABLES_NPM_PACKAGE: &str = "css-variable-lsp"; const CSS_VARIABLES_RELEASE_REPO: &str = "lmn451/css-lsp-rust"; -const CSS_VARIABLES_RELEASE_TAG: &str = "latest"; const CSS_VARIABLES_CACHE_PREFIX: &str = "css-variable-lsp-"; +#[derive(Clone, Debug, PartialEq, Eq)] +enum PathBinaryTarget { + MatchVersion(String), + // npm latest lookup failed (offline/network issue) - allow PATH if it has a readable version. + AllowWhenVersionReadable, + // Explicit dist-tags (e.g. beta) should use npm fallback instead of PATH. + Reject, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct ParsedSemver { + major: u64, + minor: u64, + patch: u64, + pre_release: Option, +} + +#[derive(Clone, Debug)] +struct CachedDirCandidate { + name: String, + path: PathBuf, + semver: Option, +} + struct CssVariablesExtension { cached_binary_path: Option, } @@ -27,36 +51,68 @@ impl CssVariablesExtension { } let (platform, arch) = zed::current_platform(); - - // 2) Check cached binary - if let Some(path) = &self.cached_binary_path { - if fs::metadata(path).is_ok_and(|stat| stat.is_file()) { - return Ok(path.clone()); - } - } - let binary_name = binary_name_for_platform(platform); + match self.resolve_latest_rust_binary(language_server_id, platform, arch, binary_name) { + Ok(path) => { + self.cached_binary_path = Some(path.clone()); + Ok(path) + } + Err(latest_err) => { + if let Some(path) = self.valid_cached_binary_path() { + self.cached_binary_path = Some(path.clone()); + return Ok(path); + } - // Resolve "latest" tag to actual version - let version_dir = format!("{CSS_VARIABLES_CACHE_PREFIX}{CSS_VARIABLES_RELEASE_TAG}"); + if let Some(path) = find_any_cached_binary(CSS_VARIABLES_CACHE_PREFIX, binary_name)? + { + self.cached_binary_path = Some(path.clone()); + return Ok(path); + } - // 2) Already downloaded Rust binary - if let Some(path) = find_binary_in_dir(&version_dir, binary_name)? { - self.cached_binary_path = Some(path.clone()); - return Ok(path); + Err(latest_err) + } } + } - // 3) Download pinned Rust release + fn resolve_latest_rust_binary( + &self, + language_server_id: &zed::LanguageServerId, + platform: zed::Os, + arch: zed::Architecture, + binary_name: &str, + ) -> zed::Result { zed::set_language_server_installation_status( language_server_id, &zed::LanguageServerInstallationStatus::CheckingForUpdate, ); + let release = zed::latest_github_release( + CSS_VARIABLES_RELEASE_REPO, + zed::GithubReleaseOptions { + require_assets: true, + pre_release: false, + }, + )?; + let asset_name = asset_name_for_platform(platform, arch)?; + let version_dir = cache_dir_for_release_version(&release.version); + + if let Some(path) = find_binary_in_dir(&version_dir, binary_name)? { + return Ok(path); + } + + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| { + format!( + "latest release '{}' missing expected asset '{}'", + release.version, asset_name + ) + })?; + let (download_type, is_archive) = download_type_for_asset(asset_name); - let download_url = format!( - "https://github.com/{CSS_VARIABLES_RELEASE_REPO}/releases/download/{CSS_VARIABLES_RELEASE_TAG}/{asset_name}" - ); fs::create_dir_all(&version_dir) .map_err(|err| format!("failed to create directory '{version_dir}': {err}"))?; @@ -66,7 +122,7 @@ impl CssVariablesExtension { ); let binary_path = if is_archive { - zed::download_file(&download_url, &version_dir, download_type) + zed::download_file(&asset.download_url, &version_dir, download_type) .map_err(|err| format!("failed to download {asset_name}: {err}"))?; find_binary_in_dir(&version_dir, binary_name)?.ok_or_else(|| { @@ -75,7 +131,7 @@ impl CssVariablesExtension { } else { let binary_path = format!("{version_dir}/{binary_name}"); if !Path::new(&binary_path).exists() { - zed::download_file(&download_url, &binary_path, download_type) + zed::download_file(&asset.download_url, &binary_path, download_type) .map_err(|err| format!("failed to download {asset_name}: {err}"))?; } binary_path @@ -86,9 +142,17 @@ impl CssVariablesExtension { } prune_cached_versions(CSS_VARIABLES_CACHE_PREFIX, &version_dir); - self.cached_binary_path = Some(binary_path.clone()); Ok(binary_path) } + + fn valid_cached_binary_path(&self) -> Option { + self.cached_binary_path.as_ref().and_then(|path| { + fs::metadata(path) + .ok() + .filter(|stat| stat.is_file()) + .map(|_| path.clone()) + }) + } } impl zed::Extension for CssVariablesExtension { @@ -115,7 +179,7 @@ impl zed::Extension for CssVariablesExtension { .as_ref() .and_then(|lsp_settings| lsp_settings.binary.as_ref()); - // Try Rust binary first, then PATH, then fall back to npm + // Try Rust binary first, then PATH if it's current, then fall back to npm let command = match self.resolve_css_variables_binary( language_server_id, worktree, @@ -126,7 +190,12 @@ impl zed::Extension for CssVariablesExtension { Err(_rust_err) => { // 4) Check PATH before npm fallback (user's own install) if let Some(path) = worktree.which(CSS_VARIABLES_BINARY_NAME) { - path + if should_use_path_binary(&path, user_settings.as_ref()) { + path + } else { + // 5) npm fallback (and update) + return build_npm_fallback_command(worktree, user_settings); + } } else { // 5) npm fallback return build_npm_fallback_command(worktree, user_settings); @@ -228,7 +297,7 @@ fn build_npm_fallback_command( worktree: &zed::Worktree, user_settings: Option, ) -> zed::Result { - let package = "css-variable-lsp"; + let package = CSS_VARIABLES_NPM_PACKAGE; let npm_version = npm_version_from_settings(user_settings.as_ref()).unwrap_or_else(|| "latest".to_string()); @@ -377,6 +446,129 @@ fn is_npm_version(value: &str) -> bool { .unwrap_or(false) } +fn should_use_path_binary(path: &str, user_settings: Option<&Value>) -> bool { + let Some(path_version) = read_binary_version(path) else { + return false; + }; + let target = expected_path_binary_target(user_settings); + should_use_path_binary_for_target(&path_version, &target) +} + +fn should_use_path_binary_for_target(path_version: &str, target: &PathBinaryTarget) -> bool { + match target { + PathBinaryTarget::MatchVersion(target_version) => { + normalize_semver_token(path_version) == normalize_semver_token(target_version) + } + PathBinaryTarget::AllowWhenVersionReadable => true, + PathBinaryTarget::Reject => false, + } +} + +fn expected_path_binary_target(user_settings: Option<&Value>) -> PathBinaryTarget { + let npm_version = + npm_version_from_settings(user_settings).unwrap_or_else(|| "latest".to_string()); + if npm_version == "latest" { + match zed::npm_package_latest_version(CSS_VARIABLES_NPM_PACKAGE) { + Ok(version) => PathBinaryTarget::MatchVersion(version), + Err(_) => PathBinaryTarget::AllowWhenVersionReadable, + } + } else if is_npm_version(&npm_version) { + PathBinaryTarget::MatchVersion(npm_version) + } else { + // Dist tags (e.g. beta) are treated as stale for PATH checks. + PathBinaryTarget::Reject + } +} + +fn read_binary_version(path: &str) -> Option { + let candidates = ["--version", "-V"]; + for arg in candidates { + if let Some(version) = read_binary_version_with_arg(path, arg) { + return Some(version); + } + } + None +} + +fn read_binary_version_with_arg(path: &str, arg: &str) -> Option { + let mut command = zed::process::Command::new(path.to_string()).arg(arg); + let output = command.output().ok()?; + if output.status != Some(0) { + return None; + } + + let mut combined = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + + if combined.is_empty() { + combined = stderr; + } else if !stderr.is_empty() { + combined.push('\n'); + combined.push_str(&stderr); + } + + extract_semver_token(&combined) +} + +fn extract_semver_token(output: &str) -> Option { + let bytes = output.as_bytes(); + let mut i = 0usize; + while i < bytes.len() { + if !bytes[i].is_ascii_digit() { + i += 1; + continue; + } + + let start = i; + i = consume_digits(bytes, i); + if i >= bytes.len() || bytes[i] != b'.' { + continue; + } + + i += 1; + let minor_start = i; + i = consume_digits(bytes, i); + if i == minor_start || i >= bytes.len() || bytes[i] != b'.' { + continue; + } + + i += 1; + let patch_start = i; + i = consume_digits(bytes, i); + if i == patch_start { + continue; + } + + while i < bytes.len() && is_semver_tail_byte(bytes[i]) { + i += 1; + } + + let token = output[start..i].to_string(); + return Some(token); + } + + None +} + +fn consume_digits(bytes: &[u8], mut index: usize) -> usize { + while index < bytes.len() && bytes[index].is_ascii_digit() { + index += 1; + } + index +} + +fn is_semver_tail_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'+') +} + +fn normalize_semver_token(version: &str) -> String { + version + .trim() + .trim_start_matches('v') + .trim_start_matches('V') + .to_string() +} + fn binary_name_for_platform(platform: zed::Os) -> &'static str { match platform { zed::Os::Windows => "css-variable-lsp.exe", @@ -415,6 +607,170 @@ fn download_type_for_asset(asset_name: &str) -> (zed::DownloadedFileType, bool) } } +fn cache_dir_for_release_version(version: &str) -> String { + let normalized = normalize_semver_token(version); + let sanitized = normalized + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_' { + c + } else { + '_' + } + }) + .collect::(); + if sanitized.is_empty() { + format!("{CSS_VARIABLES_CACHE_PREFIX}unknown") + } else { + format!("{CSS_VARIABLES_CACHE_PREFIX}{sanitized}") + } +} + +fn parse_semver_from_cache_dir_name(prefix: &str, name: &str) -> Option { + let suffix = name.strip_prefix(prefix)?; + parse_semver_token(suffix) +} + +fn parse_semver_token(version: &str) -> Option { + let normalized = normalize_semver_token(version); + let without_build = normalized + .split_once('+') + .map(|(core, _)| core) + .unwrap_or(normalized.as_str()); + let (core, pre_release) = without_build + .split_once('-') + .map(|(core, pre)| (core, Some(pre.to_string()))) + .unwrap_or((without_build, None)); + + let mut parts = core.split('.'); + let major = parts.next()?.parse::().ok()?; + let minor = parts.next()?.parse::().ok()?; + let patch = parts.next()?.parse::().ok()?; + if parts.next().is_some() { + return None; + } + + Some(ParsedSemver { + major, + minor, + patch, + pre_release, + }) +} + +fn compare_semver(a: &ParsedSemver, b: &ParsedSemver) -> std::cmp::Ordering { + use std::cmp::Ordering; + + match a.major.cmp(&b.major) { + Ordering::Equal => {} + non_eq => return non_eq, + } + match a.minor.cmp(&b.minor) { + Ordering::Equal => {} + non_eq => return non_eq, + } + match a.patch.cmp(&b.patch) { + Ordering::Equal => {} + non_eq => return non_eq, + } + + match (&a.pre_release, &b.pre_release) { + (None, None) => Ordering::Equal, + (None, Some(_)) => Ordering::Greater, + (Some(_), None) => Ordering::Less, + (Some(a_pre), Some(b_pre)) => compare_pre_release(a_pre, b_pre), + } +} + +fn compare_pre_release(a: &str, b: &str) -> std::cmp::Ordering { + use std::cmp::Ordering; + + let a_parts: Vec<&str> = a.split('.').collect(); + let b_parts: Vec<&str> = b.split('.').collect(); + let max_len = a_parts.len().max(b_parts.len()); + + for i in 0..max_len { + match (a_parts.get(i), b_parts.get(i)) { + (None, None) => return Ordering::Equal, + (None, Some(_)) => return Ordering::Less, + (Some(_), None) => return Ordering::Greater, + (Some(a_part), Some(b_part)) => { + let a_num = a_part.parse::().ok(); + let b_num = b_part.parse::().ok(); + + let part_cmp = match (a_num, b_num) { + (Some(a_val), Some(b_val)) => a_val.cmp(&b_val), + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => a_part.cmp(b_part), + }; + + if part_cmp != Ordering::Equal { + return part_cmp; + } + } + } + } + + Ordering::Equal +} + +fn compare_cached_dir_candidates( + a: &CachedDirCandidate, + b: &CachedDirCandidate, +) -> std::cmp::Ordering { + use std::cmp::Ordering; + + match (&a.semver, &b.semver) { + (Some(a_semver), Some(b_semver)) => { + let semver_cmp = compare_semver(b_semver, a_semver); + if semver_cmp == Ordering::Equal { + b.name.cmp(&a.name) + } else { + semver_cmp + } + } + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => b.name.cmp(&a.name), + } +} + +fn find_any_cached_binary(prefix: &str, binary_name: &str) -> zed::Result> { + let entries = match fs::read_dir(".") { + Ok(entries) => entries, + Err(_) => return Ok(None), + }; + + let mut cached_dirs: Vec = Vec::new(); + for entry in entries { + let entry = entry.map_err(|err| format!("failed to read directory entry: {err}"))?; + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let Some(name) = entry.file_name().to_str().map(str::to_string) else { + continue; + }; + if !name.starts_with(prefix) { + continue; + } + let semver = parse_semver_from_cache_dir_name(prefix, &name); + cached_dirs.push(CachedDirCandidate { name, path, semver }); + } + + cached_dirs.sort_by(compare_cached_dir_candidates); + + for candidate in cached_dirs { + if let Some(found) = find_binary_in_tree(&candidate.path, binary_name)? { + return Ok(Some(found)); + } + } + + Ok(None) +} + fn find_binary_in_dir(dir: &str, binary_name: &str) -> zed::Result> { let root = Path::new(dir); if !root.exists() { @@ -661,8 +1017,132 @@ mod tests { } #[test] - fn release_tag_is_latest() { - // Verify we use "latest" to always download newest LSP release - assert_eq!(CSS_VARIABLES_RELEASE_TAG, "latest"); + fn extracts_semver_from_version_output() { + assert_eq!( + extract_semver_token("css-variable-lsp 0.1.9").as_deref(), + Some("0.1.9") + ); + assert_eq!( + extract_semver_token("v0.2.0-alpha.1").as_deref(), + Some("0.2.0-alpha.1") + ); + } + + #[test] + fn rejects_invalid_semver_output() { + assert!(extract_semver_token("css-variable-lsp unknown").is_none()); + assert!(extract_semver_token("version: 0.1").is_none()); + } + + #[test] + fn normalizes_semver_tokens() { + assert_eq!(normalize_semver_token("v1.2.3"), "1.2.3"); + assert_eq!(normalize_semver_token("V1.2.3-beta.1"), "1.2.3-beta.1"); + } + + #[test] + fn compares_path_version_to_target_version() { + assert_eq!( + normalize_semver_token("v0.1.6"), + normalize_semver_token("0.1.6") + ); + assert_ne!( + normalize_semver_token("0.1.5"), + normalize_semver_token("0.1.6") + ); + } + + #[test] + fn path_target_allows_when_latest_lookup_fails() { + let target = PathBinaryTarget::AllowWhenVersionReadable; + assert!(should_use_path_binary_for_target("0.1.6", &target)); + } + + #[test] + fn path_target_rejects_for_dist_tags() { + let target = PathBinaryTarget::Reject; + assert!(!should_use_path_binary_for_target("0.1.6", &target)); + } + + #[test] + fn path_target_requires_exact_match_when_known() { + let target = PathBinaryTarget::MatchVersion("0.1.6".to_string()); + assert!(should_use_path_binary_for_target("v0.1.6", &target)); + assert!(!should_use_path_binary_for_target("0.1.5", &target)); + } + + #[test] + fn semver_compare_uses_numeric_ordering() { + let older = parse_semver_token("0.9.0").unwrap(); + let newer = parse_semver_token("0.10.0").unwrap(); + assert_eq!(compare_semver(&newer, &older), std::cmp::Ordering::Greater); + } + + #[test] + fn semver_compare_prefers_stable_over_prerelease() { + let prerelease = parse_semver_token("1.0.0-beta.1").unwrap(); + let stable = parse_semver_token("1.0.0").unwrap(); + assert_eq!( + compare_semver(&stable, &prerelease), + std::cmp::Ordering::Greater + ); + } + + #[test] + fn semver_compare_orders_prerelease_identifiers() { + let alpha1 = parse_semver_token("1.0.0-alpha.1").unwrap(); + let alpha2 = parse_semver_token("1.0.0-alpha.2").unwrap(); + assert_eq!( + compare_semver(&alpha2, &alpha1), + std::cmp::Ordering::Greater + ); + } + + #[test] + fn cached_dir_sort_prefers_newest_semver_over_lexical_order() { + let mut candidates = vec![ + CachedDirCandidate { + name: "css-variable-lsp-0.9.0".to_string(), + path: PathBuf::from("old"), + semver: parse_semver_from_cache_dir_name( + CSS_VARIABLES_CACHE_PREFIX, + "css-variable-lsp-0.9.0", + ), + }, + CachedDirCandidate { + name: "css-variable-lsp-0.10.0".to_string(), + path: PathBuf::from("new"), + semver: parse_semver_from_cache_dir_name( + CSS_VARIABLES_CACHE_PREFIX, + "css-variable-lsp-0.10.0", + ), + }, + CachedDirCandidate { + name: "css-variable-lsp-latest".to_string(), + path: PathBuf::from("fallback"), + semver: parse_semver_from_cache_dir_name( + CSS_VARIABLES_CACHE_PREFIX, + "css-variable-lsp-latest", + ), + }, + ]; + + candidates.sort_by(compare_cached_dir_candidates); + + assert_eq!(candidates[0].name, "css-variable-lsp-0.10.0"); + assert_eq!(candidates[1].name, "css-variable-lsp-0.9.0"); + assert_eq!(candidates[2].name, "css-variable-lsp-latest"); + } + + #[test] + fn builds_cache_dir_from_release_version() { + assert_eq!( + cache_dir_for_release_version("v0.1.9"), + "css-variable-lsp-0.1.9" + ); + assert_eq!( + cache_dir_for_release_version("release/0.2.0"), + "css-variable-lsp-release_0.2.0" + ); } } diff --git a/test_extension.sh b/test_extension.sh index e7f4eda..75fde30 100755 --- a/test_extension.sh +++ b/test_extension.sh @@ -74,19 +74,19 @@ echo -e "${GREEN}✓ WASM file valid (${WASM_SIZE} bytes)${NC}" # Test 4: Check Rust source for correct version echo -e "\n${YELLOW}Test 4: Verifying LSP release settings in source...${NC}" -if ! grep -q 'CSS_VARIABLES_RELEASE_TAG' src/lib.rs; then - echo -e "${RED}❌ Release tag not defined in src/lib.rs${NC}" - exit 1 -fi if ! grep -q 'CSS_VARIABLES_RELEASE_REPO' src/lib.rs; then echo -e "${RED}❌ Release repo not defined in src/lib.rs${NC}" exit 1 fi +if ! grep -q 'latest_github_release' src/lib.rs; then + echo -e "${RED}❌ latest GitHub release resolution not defined in src/lib.rs${NC}" + exit 1 +fi if ! grep -q 'build_npm_fallback_command' src/lib.rs; then echo -e "${RED}❌ npm fallback not defined in src/lib.rs${NC}" exit 1 fi -echo -e "${GREEN}✓ Source code release settings present (with npm fallback)${NC}" +echo -e "${GREEN}✓ Source code release settings present (latest + npm fallback)${NC}" # Test 5: Verify example files exist for testing echo -e "\n${YELLOW}Test 5: Checking example files...${NC}"