дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(editor): complete dependency versions in go.mod (#1871)
go.mod require entries (single-line and block) now get version completion. gopls doesn't provide module-file completions, so the existing dependency-version completion queries `go list -m -versions` with the workspace member's Go SDK and proxy settings, from a temp directory with GOWORK=off so a half-edited go.mod doesn't block it. The query has a 10 s deadline and honours cancellation; failures return no versions and aren't cached. The cache key for Go hashes root and environment so proxy credentials are never stored in clear. Co-authored-by: shohruh-genius <[email protected]>
Цей коміт міститься в:
1 джерело
429b8f0efe
коміт
ebc1c21836
9 files changed
+386
-24
No files matched your search
@@ -28,7 +28,7 @@ pub async fn debug_start(host: &Host, window: &str, config: LaunchConfig) -> Ide
|
||||
let settings = settings.clone();
|
||||
config = host
|
||||
.blocking(move || {
|
||||
ide_sdk::go::debug_environment(&root, &settings, &mut config.env);
|
||||
ide_sdk::go::configure_environment(&root, &settings, &mut config.env);
|
||||
ide_sdk::go::apply_to_debug(
|
||||
&mut config.env,
|
||||
&mut config.extra_json,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
//! Repository version lookups for manifest inspections and completion.
|
||||
use crate::{Host, rpc::Dispatcher};
|
||||
use ide_core::{CancellationToken, DocId, IdeError, IdeResult, RequestId};
|
||||
use ide_workspace::dependencies::{DependencyVersionSource, DependencyVersions, cargo};
|
||||
use ide_workspace::dependencies::{DependencyVersionSource, DependencyVersions, cargo, go};
|
||||
use std::collections::BTreeMap;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
struct CratesIo;
|
||||
impl DependencyVersionSource for CratesIo {
|
||||
@@ -21,6 +25,76 @@ impl DependencyVersionSource for CratesIo {
|
||||
}
|
||||
}
|
||||
|
||||
struct GoModules {
|
||||
root: PathBuf,
|
||||
env: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl DependencyVersionSource for GoModules {
|
||||
fn id(&self) -> &'static str {
|
||||
"go-modules"
|
||||
}
|
||||
|
||||
fn cache_key(&self, package: &str) -> Option<String> {
|
||||
let mut hash = DefaultHasher::new();
|
||||
(&self.root, &self.env, package).hash(&mut hash);
|
||||
Some(format!("{}-{:016x}", self.id(), hash.finish()))
|
||||
}
|
||||
|
||||
fn fetch(&self, package: &str, cancel: &CancellationToken) -> IdeResult<Vec<String>> {
|
||||
let search =
|
||||
self.env.get("PATH").map(std::ffi::OsString::from).unwrap_or_else(ide_core::tool_path::search_path);
|
||||
let go = ide_core::tool_path::find_command("go", &search, &self.root)
|
||||
.ok_or_else(|| IdeError::unavailable("Go SDK is unavailable"))?;
|
||||
// A partially edited go.mod is often invalid. Query outside it while
|
||||
// keeping the SDK and proxy/private module configuration.
|
||||
let dir = tempfile::tempdir()?;
|
||||
let mut cmd = Command::new(go);
|
||||
cmd.current_dir(dir.path())
|
||||
.env("PATH", search)
|
||||
.envs(&self.env)
|
||||
.env("GO111MODULE", "on")
|
||||
.env("GOFLAGS", "")
|
||||
.env("GOWORK", "off")
|
||||
.env("GIT_TERMINAL_PROMPT", "0")
|
||||
.args(["list", "-m", "-versions", "-f", "{{range .Versions}}{{println .}}{{end}}", "--", package]);
|
||||
let query = cancel.child_token();
|
||||
// Fetches run on Host::blocking's Tokio pool, which keeps a runtime current.
|
||||
let deadline = tokio::spawn({
|
||||
let query = query.clone();
|
||||
async move {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||
query.cancel();
|
||||
}
|
||||
});
|
||||
let output = ide_tools::run_streamed(cmd, &query, &|_| {});
|
||||
deadline.abort();
|
||||
Ok(go::listed_versions(&output?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
#[path = "dependencies/go_tests.rs"]
|
||||
mod go_tests;
|
||||
|
||||
fn go_source(project: &crate::Project, config_dir: &Path, path: &Path) -> IdeResult<GoModules> {
|
||||
let member = ide_workspace::workspace::Workspace::load(&project.root)?.and_then(|workspace| {
|
||||
workspace
|
||||
.projects
|
||||
.into_iter()
|
||||
.filter(|root| path.starts_with(root))
|
||||
.max_by_key(|root| root.components().count())
|
||||
});
|
||||
let settings = match &member {
|
||||
Some(root) => ide_workspace::SettingsStore::load(config_dir, Some(root)).effective(),
|
||||
None => project.settings.effective(),
|
||||
};
|
||||
let root = member.unwrap_or_else(|| project.root.clone());
|
||||
let mut env = BTreeMap::new();
|
||||
ide_sdk::go::configure_environment(&root, &settings, &mut env);
|
||||
Ok(GoModules { root, env })
|
||||
}
|
||||
|
||||
/// `offset` limits a completion request to its version string; inspections omit it.
|
||||
pub async fn dependency_versions(
|
||||
host: &Host,
|
||||
@@ -31,11 +105,24 @@ pub async fn dependency_versions(
|
||||
) -> IdeResult<Vec<DependencyVersions>> {
|
||||
let project = host.project(window)?;
|
||||
let cache = host.dependency_versions.clone();
|
||||
let cancel = host.begin_request(request);
|
||||
let config_dir = host.config_dir.clone();
|
||||
let cancel = host.begin_project_read_request(window, request);
|
||||
let result = host
|
||||
.blocking(move || {
|
||||
let snap = project.documents.snapshot(doc)?;
|
||||
if snap.large || snap.read_only || snap.path.file_name().is_none_or(|n| n != "Cargo.toml") {
|
||||
if snap.large || snap.read_only {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if snap.path.file_name().is_some_and(|name| name == "go.mod") {
|
||||
let Some(dependency) = offset.and_then(|offset| go::dependency_at(&snap.text, offset)) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
project.require_trust()?;
|
||||
let source = go_source(&project, &config_dir, snap.path.as_ref())?;
|
||||
let versions = cache.versions(&source, &dependency.package, &cancel)?;
|
||||
return Ok(vec![DependencyVersions { dependency, versions, update: None }]);
|
||||
}
|
||||
if snap.path.file_name().is_none_or(|name| name != "Cargo.toml") {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
use super::*;
|
||||
use ide_workspace::dependencies::VersionCache;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
fn source(dir: &Path, script: &str) -> GoModules {
|
||||
let bin = dir.join("bin");
|
||||
std::fs::create_dir_all(&bin).unwrap();
|
||||
let go = bin.join("go");
|
||||
std::fs::write(&go, format!("#!/bin/sh\n{script}\n")).unwrap();
|
||||
std::fs::set_permissions(&go, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
GoModules {
|
||||
root: dir.to_owned(),
|
||||
env: BTreeMap::from([
|
||||
("PATH".into(), bin.to_string_lossy().into_owned()),
|
||||
("PROJECT_ROOT".into(), dir.to_string_lossy().into_owned()),
|
||||
("GOPROXY".into(), "https://user:[email protected]".into()),
|
||||
("GOPRIVATE".into(), "private.example/token".into()),
|
||||
("GOWORK".into(), "broken.work".into()),
|
||||
("GOFLAGS".into(), "-mod=vendor".into()),
|
||||
]),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn go_query_is_isolated_and_persists_only_hashed_configuration_and_versions() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let source = source(
|
||||
dir.path(),
|
||||
r#"set -eu
|
||||
[ "$PWD" != "$PROJECT_ROOT" ]
|
||||
[ "$GOPROXY" = 'https://user:[email protected]' ]
|
||||
[ "$GOPRIVATE" = 'private.example/token' ]
|
||||
[ "$GOWORK" = off ]
|
||||
[ "$GOFLAGS" = '' ]
|
||||
[ "$GO111MODULE" = on ]
|
||||
[ "$GIT_TERMINAL_PROMPT" = 0 ]
|
||||
[ "$1 $2 $3 $4" = 'list -m -versions -f' ]
|
||||
[ "$5" = '{{range .Versions}}{{println .}}{{end}}' ]
|
||||
[ "$6 $7" = '-- example.com/Org/Lib/v2' ]
|
||||
printf 'v2.9.0\nv2.10.0\n'"#,
|
||||
);
|
||||
let key = source.cache_key("example.com/Org/Lib/v2").unwrap();
|
||||
assert_ne!(key, source.cache_key("example.com/org/lib/v2").unwrap());
|
||||
let mut other = GoModules { root: source.root.join("member"), env: source.env.clone() };
|
||||
assert_ne!(key, other.cache_key("example.com/Org/Lib/v2").unwrap());
|
||||
other.root = source.root.clone();
|
||||
other.env.insert("GOPROXY".into(), "https://other.example".into());
|
||||
assert_ne!(key, other.cache_key("example.com/Org/Lib/v2").unwrap());
|
||||
let cache_dir = dir.path().join("cache");
|
||||
let disk = cache_dir.clone();
|
||||
let versions = tokio::task::spawn_blocking(move || {
|
||||
VersionCache::new(cache_dir).versions(&source, "example.com/Org/Lib/v2", &CancellationToken::new())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(versions, ["v2.10.0", "v2.9.0"]);
|
||||
let entries: Vec<_> = std::fs::read_dir(disk).unwrap().map(Result::unwrap).collect();
|
||||
assert_eq!(entries.len(), 1, "the hashed key passes the cache filename filter");
|
||||
assert_eq!(entries[0].file_name().to_str().unwrap(), format!("{key}.json"));
|
||||
let cached = std::fs::read_to_string(entries[0].path()).unwrap();
|
||||
for sensitive in ["secret", "proxy.example", "private.example", "token", "PROJECT_ROOT"] {
|
||||
assert!(!cached.contains(sensitive), "configuration must not be persisted: {sensitive}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn failed_go_queries_are_silent_and_not_persisted() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let source = source(dir.path(), "echo 'private module or offline proxy' >&2\nexit 1");
|
||||
let cache_dir = dir.path().join("cache");
|
||||
let disk = cache_dir.clone();
|
||||
let versions = tokio::task::spawn_blocking(move || {
|
||||
VersionCache::new(cache_dir).versions(&source, "private.example/module", &CancellationToken::new())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(versions.is_empty());
|
||||
assert!(!disk.exists());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn stalled_go_queries_obey_the_deadline_and_parent_cancellation() {
|
||||
for cancel_parent in [false, true] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let source = source(dir.path(), "exec /bin/sleep 60");
|
||||
let cancel = CancellationToken::new();
|
||||
let query = cancel.clone();
|
||||
let started = Instant::now();
|
||||
let work = tokio::task::spawn_blocking(move || {
|
||||
VersionCache::new(source.root.join("cache")).versions(&source, "example.com/stalled", &query)
|
||||
});
|
||||
if cancel_parent {
|
||||
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||
cancel.cancel();
|
||||
assert!(work.await.unwrap().is_err());
|
||||
assert!(started.elapsed() < Duration::from_secs(3));
|
||||
} else {
|
||||
assert!(work.await.unwrap().unwrap().is_empty());
|
||||
assert!(started.elapsed() >= Duration::from_secs(10));
|
||||
assert!(started.elapsed() < Duration::from_secs(15));
|
||||
assert!(!cancel.is_cancelled(), "a query deadline must not cancel its caller");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,7 +228,7 @@ mod command_tests {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn debug_environment(
|
||||
pub fn configure_environment(
|
||||
root: &std::path::Path,
|
||||
settings: &ide_core::settings::Settings,
|
||||
env: &mut std::collections::BTreeMap<String, String>,
|
||||
|
||||
@@ -39,11 +39,11 @@ impl VersionCache {
|
||||
package: &str,
|
||||
cancel: &CancellationToken,
|
||||
) -> IdeResult<Vec<String>> {
|
||||
let safe = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_alphanumeric() || b"-_".contains(&b));
|
||||
if !safe(source.id()) || !safe(package) || package.len() > 64 {
|
||||
let Some(key) = source.cache_key(package).filter(|key| {
|
||||
!key.is_empty() && key.len() <= 128 && key.bytes().all(|b| b.is_ascii_alphanumeric() || b"-_".contains(&b))
|
||||
}) else {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let key = format!("{}-{}", source.id(), package.to_ascii_lowercase());
|
||||
};
|
||||
let slot = self.entries.lock().entry(key.clone()).or_default().clone();
|
||||
let mut cached = loop {
|
||||
if cancel.is_cancelled() {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
use super::ManifestDependency;
|
||||
|
||||
fn module_path(path: &str) -> bool {
|
||||
let Some(domain) = path.split('/').next() else { return false };
|
||||
path.len() <= 512
|
||||
&& domain.contains('.')
|
||||
&& !domain.starts_with(['.', '-'])
|
||||
&& path.split('/').all(|part| {
|
||||
!part.is_empty()
|
||||
&& !part.ends_with('.')
|
||||
&& part.bytes().all(|c| c.is_ascii_alphanumeric() || b"-._~".contains(&c))
|
||||
})
|
||||
}
|
||||
|
||||
/// Finds the version at a UTF-16 offset, including incomplete require lines.
|
||||
pub fn dependency_at(text: &str, offset: u32) -> Option<ManifestDependency> {
|
||||
let mut block = None;
|
||||
let mut start = 0;
|
||||
for line in text.split_inclusive('\n') {
|
||||
let line_len = line.encode_utf16().count() as u32;
|
||||
let code = line.split("//").next()?.trim_end_matches(['\r', '\n']);
|
||||
let trimmed = code.trim();
|
||||
if offset < start + line_len || offset == start + line_len && !line.ends_with('\n') {
|
||||
let body = match block {
|
||||
Some("require") => code.trim_start(),
|
||||
None => code.trim_start().strip_prefix("require")?.strip_prefix([' ', '\t'])?.trim_start(),
|
||||
_ => return None,
|
||||
};
|
||||
let (path, rest) = body.split_once([' ', '\t'])?;
|
||||
let package = path.trim_matches('"');
|
||||
if !module_path(package) {
|
||||
return None;
|
||||
}
|
||||
let version = rest.trim_start();
|
||||
let token = version.split_ascii_whitespace().next().unwrap_or_default();
|
||||
let from = start + code[..code.len() - version.len()].encode_utf16().count() as u32;
|
||||
let (requirement, from) = match token.strip_prefix('"') {
|
||||
Some(quoted) => (quoted.strip_suffix('"').unwrap_or(quoted), from + 1),
|
||||
None => (token, from),
|
||||
};
|
||||
let to = from + requirement.encode_utf16().count() as u32;
|
||||
return (from <= offset && offset <= to).then(|| ManifestDependency {
|
||||
package: package.to_owned(),
|
||||
requirement: requirement.to_owned(),
|
||||
from,
|
||||
to,
|
||||
});
|
||||
}
|
||||
if trimmed == ")" {
|
||||
block = None;
|
||||
} else if block.is_none() {
|
||||
block = trimmed.strip_suffix('(').map(str::trim);
|
||||
}
|
||||
start += line_len;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Go lists versions in ascending semantic order. Ignore tool output unrelated to versions.
|
||||
pub fn listed_versions(output: &str) -> Vec<String> {
|
||||
output
|
||||
.lines()
|
||||
.rev()
|
||||
.map(str::trim)
|
||||
.filter(|line| line.strip_prefix('v').is_some_and(|version| semver::Version::parse(version).is_ok()))
|
||||
.map(str::to_owned)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn completes_blank_partial_and_quoted_require_versions() {
|
||||
for (text, package, version) in [
|
||||
("module example.com/m\nrequire golang.org/x/text |", "golang.org/x/text", ""),
|
||||
(
|
||||
"// 中文 😀\r\nrequire (\r\n\tgithub.com/Org/Lib/v2 v2.|1.0 // indirect\r\n)\r\n",
|
||||
"github.com/Org/Lib/v2",
|
||||
"v2.1.0",
|
||||
),
|
||||
("require \"rsc.io/quote\" \"v1.|2.0\"", "rsc.io/quote", "v1.2.0"),
|
||||
] {
|
||||
let offset = text.split_once('|').unwrap().0.encode_utf16().count() as u32;
|
||||
let text = text.replace('|', "");
|
||||
let item = dependency_at(&text, offset).unwrap();
|
||||
assert_eq!(item.package, package);
|
||||
assert_eq!(item.requirement, version);
|
||||
let units: Vec<_> = text.encode_utf16().collect();
|
||||
assert_eq!(String::from_utf16(&units[item.from as usize..item.to as usize]).unwrap(), version);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_comments_other_directives_and_non_module_arguments() {
|
||||
for text in [
|
||||
"// require rsc.io/quote v|",
|
||||
"require rsc.io/quote v1.2.0 // in|direct",
|
||||
"require rsc.io/qu|ote v1.2.0",
|
||||
"replace (\n\trsc.io/quote v|\n)",
|
||||
"require ../quote v|",
|
||||
"require -flag.example v|",
|
||||
"require rsc.io/quote@latest v|",
|
||||
] {
|
||||
let offset = text.split_once('|').unwrap().0.encode_utf16().count() as u32;
|
||||
assert!(dependency_at(&text.replace('|', ""), offset).is_none(), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_go_version_spelling_and_prefers_newer_versions() {
|
||||
assert_eq!(
|
||||
listed_versions("go: notice\nv1.9.0\nv1.10.0-rc.1\nv2.0.0+incompatible\n"),
|
||||
["v2.0.0+incompatible", "v1.10.0-rc.1", "v1.9.0"]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Manifest dependencies and repository versions, independent of editor and transport.
|
||||
mod cache;
|
||||
pub mod cargo;
|
||||
pub mod go;
|
||||
|
||||
pub use cache::VersionCache;
|
||||
use ide_core::{CancellationToken, IdeResult};
|
||||
@@ -11,6 +12,12 @@ use specta::Type;
|
||||
pub trait DependencyVersionSource: Send + Sync {
|
||||
/// A filesystem-safe, stable repository identifier.
|
||||
fn id(&self) -> &'static str;
|
||||
/// Override for case-sensitive module paths or project-specific repositories.
|
||||
fn cache_key(&self, package: &str) -> Option<String> {
|
||||
let safe = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_alphanumeric() || b"-_".contains(&b));
|
||||
let id = self.id();
|
||||
(safe(id) && safe(package) && package.len() <= 64).then(|| format!("{id}-{}", package.to_ascii_lowercase()))
|
||||
}
|
||||
/// Non-yanked versions in descending semantic order.
|
||||
fn fetch(&self, package: &str, cancel: &CancellationToken) -> IdeResult<Vec<String>>;
|
||||
}
|
||||
|
||||
@@ -46,14 +46,14 @@ afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
function editor(path = "/p/Cargo.toml", readOnly = false) {
|
||||
function editor(path = "/p/Cargo.toml", readOnly = false, source = text) {
|
||||
const ctx = { doc: 7, path, language: "toml", uiMode: "TOML", readOnly };
|
||||
const contribution = editorContributions().find((c) => c.id === "dependency-versions")!;
|
||||
const view = new EditorView({
|
||||
parent: document.body,
|
||||
state: EditorState.create({
|
||||
doc: text,
|
||||
selection: { anchor: from },
|
||||
doc: source,
|
||||
selection: { anchor: Math.min(from, source.length) },
|
||||
extensions: [lspDoc.of(ctx), contribution.extensions(ctx)],
|
||||
}),
|
||||
});
|
||||
@@ -208,3 +208,37 @@ describe("Cargo dependency versions", () => {
|
||||
expect(result.options[0]).toMatchObject({ label: "2.0.0-beta.1", apply: "^2.0.0-beta.1" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Go module dependency versions", () => {
|
||||
it("requests versions at the caret and replaces the entire version without running inspections", async () => {
|
||||
const source = "module example.com/app\n\nrequire (\n\tgolang.org/x/text v0.1.0 // indirect\n)\n";
|
||||
const from = source.indexOf("v0.1.0");
|
||||
const to = from + "v0.1.0".length;
|
||||
const view = editor("/p/go.mod", false, source);
|
||||
await settle();
|
||||
expect(dependencyVersions).not.toHaveBeenCalled();
|
||||
dependencyVersions.mockResolvedValue({
|
||||
status: "ok",
|
||||
data: [
|
||||
{
|
||||
dependency: { package: "golang.org/x/text", requirement: "v0.1.0", from, to },
|
||||
versions: ["v1.2.0", "v0.12.0", "v0.1.0"],
|
||||
update: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
const complete = view.state.languageDataAt<(ctx: CompletionContext) => Promise<unknown>>(
|
||||
"openideaCompletion",
|
||||
from,
|
||||
)[0]!;
|
||||
expect(await complete(new CompletionContext(view.state, from + 3, false, view))).toMatchObject({
|
||||
from,
|
||||
to,
|
||||
options: [
|
||||
{ label: "v0.12.0", apply: "v0.12.0" },
|
||||
{ label: "v0.1.0", apply: "v0.1.0" },
|
||||
],
|
||||
});
|
||||
expect(dependencyVersions).toHaveBeenCalledExactlyOnceWith(7, from + 3, expect.any(Number));
|
||||
});
|
||||
});
|
||||
@@ -168,12 +168,25 @@ function extension(doc: EditorDocContext): Extension {
|
||||
},
|
||||
};
|
||||
});
|
||||
return [
|
||||
versionsField,
|
||||
plugin,
|
||||
intentionProviders.of((view) => {
|
||||
const selection = view.state.selection.main;
|
||||
return dependencyFixes(view, selection.from, selection.to);
|
||||
}),
|
||||
quickFixProviders.of(dependencyFixes),
|
||||
versionCompletion(doc),
|
||||
];
|
||||
}
|
||||
|
||||
function versionCompletion(doc: EditorDocContext): Extension {
|
||||
const completion = async (ctx: CompletionContext): Promise<CompletionResult | null> => {
|
||||
if (!isTauri()) return null;
|
||||
const id = nextRequestId();
|
||||
const text = ctx.state.doc;
|
||||
const root = useProject.getState().project?.root;
|
||||
ctx.addEventListener("abort", () => void commands.cancelRequest(id));
|
||||
ctx.addEventListener("abort", () => void commands.cancelRequest(id), { onDocChange: true });
|
||||
try {
|
||||
// The completion context is immutable; only use it while its document is current.
|
||||
const view = ctx.view;
|
||||
@@ -188,21 +201,17 @@ function extension(doc: EditorDocContext): Extension {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
return [
|
||||
versionsField,
|
||||
plugin,
|
||||
intentionProviders.of((view) => {
|
||||
const selection = view.state.selection.main;
|
||||
return dependencyFixes(view, selection.from, selection.to);
|
||||
}),
|
||||
quickFixProviders.of(dependencyFixes),
|
||||
EditorState.languageData.of(() => [{ openideaCompletion: completion }]),
|
||||
];
|
||||
return EditorState.languageData.of(() => [{ openideaCompletion: completion }]);
|
||||
}
|
||||
|
||||
export function activate(): () => void {
|
||||
return registerEditorContribution({
|
||||
id: "dependency-versions",
|
||||
extensions: (doc) => (!doc.virtual && !doc.readOnly && /(?:^|\/)Cargo\.toml$/.test(doc.path) ? extension(doc) : []),
|
||||
extensions: (doc) => {
|
||||
if (doc.virtual || doc.readOnly) return [];
|
||||
if (/(?:^|\/)Cargo\.toml$/.test(doc.path)) return extension(doc);
|
||||
if (/(?:^|\/)go\.mod$/.test(doc.path)) return versionCompletion(doc);
|
||||
return [];
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача