Repository: denoland/deno
Pull request: feat(lsp): registry completions for import-mapped specifiers

Closes #22566

Changed files:
filename: cli/lsp/completions.rs
patch: @@ -215,16 +215,13 @@ pub async fn get_import_completions(
       module_registries,
     )
     .await;
-    let offset = if position.character > range.start.character {
-      (position.character - range.start.character) as usize
-    } else {
-      0
-    };
     let maybe_list = module_registries
-      .get_completions(&text, offset, &range, |s| {
+      .get_completions(&text, &range, resolved.as_ref(), |s| {
         documents.exists(s, file_referrer)
       })
       .await;
+    let maybe_list = maybe_list
+      .or_else(|| module_registries.get_origin_completions(&text, &range));
     let list = maybe_list.unwrap_or_else(|| CompletionList {
       items: get_workspace_completions(specifier, &text, &range, documents),
       is_incomplete: false,

filename: cli/lsp/registries.rs
patch: @@ -33,6 +33,7 @@ use deno_graph::Dependency;
 use deno_runtime::deno_permissions::PermissionsContainer;
 use log::error;
 use once_cell::sync::Lazy;
+use std::borrow::Cow;
 use std::collections::HashMap;
 use std::path::PathBuf;
 use std::sync::Arc;
@@ -86,40 +87,40 @@ enum CompletionType {
 /// Determine if a completion at a given offset is a string literal or a key/
 /// variable.
 fn get_completion_type(
-  offset: usize,
+  char_offset: usize,
   tokens: &[Token],
   match_result: &MatchResult,
 ) -> Option<CompletionType> {
-  let mut len = 0_usize;
+  let mut char_count = 0_usize;
   for (index, token) in tokens.iter().enumerate() {
     match token {
       Token::String(s) => {
-        len += s.chars().count();
-        if offset < len {
+        char_count += s.chars().count();
+        if char_offset < char_count {
           return Some(CompletionType::Literal(s.clone()));
         }
       }
       Token::Key(k) => {
         if let Some(prefix) = &k.prefix {
-          len += prefix.chars().count();
-          if offset < len {
+          char_count += prefix.chars().count();
+          if char_offset < char_count {
             return Some(CompletionType::Key {
               key: k.clone(),
               prefix: Some(prefix.clone()),
               index,
             });
           }
         }
-        if offset < len {
+        if char_offset < char_count {
           return None;
         }
         if let StringOrNumber::String(name) = &k.name {
           let value = match_result
             .get(name)
             .map(|s| s.to_string(Some(k), false))
             .unwrap_or_default();
-          len += value.chars().count();
-          if offset <= len {
+          char_count += value.chars().count();
+          if char_offset <= char_count {
             return Some(CompletionType::Key {
               key: k.clone(),
               prefix: None,
@@ -128,8 +129,8 @@ fn get_completion_type(
           }
         }
         if let Some(suffix) = &k.suffix {
-          len += suffix.chars().count();
-          if offset <= len {
+          char_count += suffix.chars().count();
+          if char_offset <= char_count {
             return Some(CompletionType::Literal(suffix.clone()));
           }
         }
@@ -449,49 +450,6 @@ impl ModuleRegistry {
     }
   }
 
-  fn complete_literal(
-    &self,
-    s: String,
-    completions: &mut HashMap<String, lsp::CompletionItem>,
-    current_specifier: &str,
-    offset: usize,
-    range: &lsp::Range,
-  ) {
-    let label = if s.starts_with('/') {
-      s[0..].to_string()
-    } else {
-      s.to_string()
-    };
-    let full_text = format!(
-      "{}{}{}",
-      &current_specifier[..offset],
-      s,
-      &current_specifier[offset..]
-    );
-    let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-      range: *range,
-      new_text: full_text.clone(),
-    }));
-    let filter_text = Some(full_text);
-    completions.insert(
-      s,
-      lsp::CompletionItem {
-        label,
-        kind: Some(lsp::CompletionItemKind::FOLDER),
-        filter_text,
-        sort_text: Some("1".to_string()),
-        text_edit,
-        commit_characters: Some(
-          REGISTRY_IMPORT_COMMIT_CHARS
-            .iter()
-            .map(|&c| c.into())
-            .collect(),
-        ),
-        ..Default::default()
-      },
-    );
-  }
-
   /// Disable a registry, removing its configuration, if any, from memory.
   pub fn disable(&mut self, origin: &str) {
     let Ok(origin_url) = Url::parse(origin) else {
@@ -654,339 +612,366 @@ impl ModuleRegistry {
   /// any, for the specifier.
   pub async fn get_completions(
     &self,
-    current_specifier: &str,
-    offset: usize,
+    text: &str,
     range: &lsp::Range,
+    resolved: Option<&ModuleSpecifier>,
     specifier_exists: impl Fn(&ModuleSpecifier) -> bool,
   ) -> Option<lsp::CompletionList> {
-    if let Ok(specifier) = Url::parse(current_specifier) {
-      let origin = base_url(&specifier);
-      let origin_len = origin.chars().count();
-      if offset >= origin_len {
-        if let Some(registries) = self.origins.get(&origin) {
-          let path = &specifier[Position::BeforePath..];
-          let path_offset = offset - origin_len;
-          let mut completions = HashMap::<String, lsp::CompletionItem>::new();
-          let mut is_incomplete = false;
-          let mut did_match = false;
-          for registry in registries {
-            let tokens = parse(&registry.schema, None)
-              .map_err(|e| {
-                error!(
-                  "Error parsing registry schema for origin \"{}\". {}",
-                  origin, e
-                );
-              })
-              .ok()?;
-            let mut i = tokens.len();
-            let last_key_name = StringOrNumber::String(
-              tokens
-                .iter()
-                .last()
-                .map(|t| {
-                  if let Token::Key(key) = t {
-                    if let StringOrNumber::String(s) = &key.name {
-                      return s.clone();
-                    }
-                  }
-                  "".to_string()
-                })
-                .unwrap_or_default(),
+    let resolved = resolved
+      .map(Cow::Borrowed)
+      .or_else(|| ModuleSpecifier::parse(text).ok().map(Cow::Owned))?;
+    let resolved_str = resolved.as_str();
+    let origin = base_url(&resolved);
+    let origin_char_count = origin.chars().count();
+    let registries = self.origins.get(&origin)?;
+    let path = &resolved[Position::BeforePath..];
+    let path_char_offset = resolved_str.chars().count() - origin_char_count;
+    let mut completions = HashMap::<String, lsp::CompletionItem>::new();
+    let mut is_incomplete = false;
+    let mut did_match = false;
+    for registry in registries {
+      let tokens = parse(&registry.schema, None)
+        .map_err(|e| {
+          error!(
+            "Error parsing registry schema for origin \"{}\". {}",
+            origin, e
+          );
+        })
+        .ok()?;
+      let mut i = tokens.len();
+      let last_key_name = StringOrNumber::String(
+        tokens
+          .iter()
+          .last()
+          .map(|t| {
+            if let Token::Key(key) = t {
+              if let StringOrNumber::String(s) = &key.name {
+                return s.clone();
+              }
+            }
+            "".to_string()
+          })
+          .unwrap_or_default(),
+      );
+      loop {
+        let matcher = Matcher::new(&tokens[..i], None)
+          .map_err(|e| {
+            error!(
+              "Error creating matcher for schema for origin \"{}\". {}",
+              origin, e
             );
-            loop {
-              let matcher = Matcher::new(&tokens[..i], None)
-                .map_err(|e| {
-                  error!(
-                    "Error creating matcher for schema for origin \"{}\". {}",
-                    origin, e
-                  );
-                })
-                .ok()?;
-              if let Some(match_result) = matcher.matches(path) {
-                did_match = true;
-                let completion_type =
-                  get_completion_type(path_offset, &tokens, &match_result);
-                match completion_type {
-                  Some(CompletionType::Literal(s)) => self.complete_literal(
-                    s,
-                    &mut completions,
-                    current_specifier,
-                    offset,
-                    range,
+          })
+          .ok()?;
+        if let Some(match_result) = matcher.matches(path) {
+          did_match = true;
+          let completion_type =
+            get_completion_type(path_char_offset, &tokens, &match_result);
+          match completion_type {
+            Some(CompletionType::Literal(s)) => {
+              let label = s;
+              let full_text = format!("{text}{label}");
+              let text_edit =
+                Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                  range: *range,
+                  new_text: full_text.clone(),
+                }));
+              let filter_text = Some(full_text);
+              completions.insert(
+                label.clone(),
+                lsp::CompletionItem {
+                  label,
+                  kind: Some(lsp::CompletionItemKind::FOLDER),
+                  filter_text,
+                  sort_text: Some("1".to_string()),
+                  text_edit,
+                  commit_characters: Some(
+                    REGISTRY_IMPORT_COMMIT_CHARS
+                      .iter()
+                      .map(|&c| c.into())
+                      .collect(),
                   ),
-                  Some(CompletionType::Key { key, prefix, index }) => {
-                    let maybe_url = registry.get_url_for_key(&key);
-                    if let Some(url) = maybe_url {
-                      if let Some(items) = self
-                        .get_variable_items(
-                          &key,
-                          url,
-                          &specifier,
-                          &tokens,
-                          &match_result,
-                        )
-                        .await
-                      {
-                        let compiler = Compiler::new(&tokens[..=index], None);
-                        let base = Url::parse(&origin).ok()?;
-                        let (items, preselect, incomplete) = match items {
-                          VariableItems::List(list) => {
-                            (list.items, list.preselect, list.is_incomplete)
-                          }
-                          VariableItems::Simple(items) => (items, None, false),
-                        };
-                        if incomplete {
-                          is_incomplete = true;
-                        }
-                        for (idx, item) in items.into_iter().enumerate() {
-                          let mut label = if let Some(p) = &prefix {
-                            format!("{p}{item}")
-                          } else {
-                            item.clone()
-                          };
-                          if label.ends_with('/') {
-                            label.pop();
-                          }
-                          let kind = if key.name == last_key_name
-                            && !item.ends_with('/')
-                          {
-                            Some(lsp::CompletionItemKind::FILE)
-                          } else {
-                            Some(lsp::CompletionItemKind::FOLDER)
-                          };
-                          let mut params = match_result.params.clone();
-                          params.insert(
-                            key.name.clone(),
-                            StringOrVec::from_str(&item, &key),
-                          );
-                          let mut path =
-                            compiler.to_path(&params).unwrap_or_default();
-                          if path.ends_with('/') {
-                            path.pop();
-                          }
-                          let item_specifier = base.join(&path).ok()?;
-                          let full_text = item_specifier.as_str();
-                          let text_edit = Some(lsp::CompletionTextEdit::Edit(
-                            lsp::TextEdit {
-                              range: *range,
-                              new_text: full_text.to_string(),
-                            },
-                          ));
-                          let command = if key.name == last_key_name
-                            && !item.ends_with('/')
-                            && !specifier_exists(&item_specifier)
-                          {
-                            Some(lsp::Command {
-                              title: "".to_string(),
-                              command: "deno.cache".to_string(),
-                              arguments: Some(vec![
-                                json!([item_specifier]),
-                                json!(&specifier),
-                              ]),
-                            })
-                          } else {
-                            None
-                          };
-                          let detail = Some(format!("({})", key.name));
-                          let filter_text = Some(full_text.to_string());
-                          let sort_text = Some(format!("{:0>10}", idx + 1));
-                          let preselect =
-                            get_preselect(item.clone(), preselect.clone());
-                          let data = get_data_with_match(
-                            registry,
-                            &specifier,
-                            &tokens,
-                            &match_result,
-                            &key,
-                            &item,
-                          );
-                          let commit_characters = if is_incomplete {
-                            Some(
-                              REGISTRY_IMPORT_COMMIT_CHARS
-                                .iter()
-                                .map(|&c| c.into())
-                                .collect(),
-                            )
-                          } else {
-                            Some(
-                              IMPORT_COMMIT_CHARS
-                                .iter()
-                                .map(|&c| c.into())
-                                .collect(),
-                            )
-                          };
-                          completions.insert(
-                            item,
-                            lsp::CompletionItem {
-                              label,
-                              kind,
-                              detail,
-                              sort_text,
-                              filter_text,
-                              text_edit,
-                              command,
-                              preselect,
-                              data,
-                              commit_characters,
-                              ..Default::default()
-                            },
-                          );
-                        }
-                      }
+                  ..Default::default()
+                },
+              );
+            }
+            Some(CompletionType::Key { key, prefix, index }) => {
+              let maybe_url = registry.get_url_for_key(&key);
+              if let Some(url) = maybe_url {
+                if let Some(items) = self
+                  .get_variable_items(
+                    &key,
+                    url,
+                    &resolved,
+                    &tokens,
+                    &match_result,
+                  )
+                  .await
+                {
+                  let compiler = Compiler::new(&tokens[..=index], None);
+                  let base = Url::parse(&origin).ok()?;
+                  let (items, preselect, incomplete) = match items {
+                    VariableItems::List(list) => {
+                      (list.items, list.preselect, list.is_incomplete)
                     }
+                    VariableItems::Simple(items) => (items, None, false),
+                  };
+                  if incomplete {
+                    is_incomplete = true;
+                  }
+                  for (idx, item) in items.into_iter().enumerate() {
+                    let mut label = if let Some(p) = &prefix {
+                      format!("{p}{item}")
+                    } else {
+                      item.clone()
+                    };
+                    if label.ends_with('/') {
+                      label.pop();
+                    }
+                    let kind =
+                      if key.name == last_key_name && !item.ends_with('/') {
+                        Some(lsp::CompletionItemKind::FILE)
+                      } else {
+                        Some(lsp::CompletionItemKind::FOLDER)
+                      };
+                    let mut params = match_result.params.clone();
+                    params.insert(
+                      key.name.clone(),
+                      StringOrVec::from_str(&item, &key),
+                    );
+                    let mut path =
+                      compiler.to_path(&params).unwrap_or_default();
+                    if path.ends_with('/') {
+                      path.pop();
+                    }
+                    let item_specifier = base.join(&path).ok()?;
+                    let full_text = if let Some(suffix) =
+                      item_specifier.as_str().strip_prefix(resolved_str)
+                    {
+                      format!("{text}{suffix}")
+                    } else {
+                      item_specifier.to_string()
+                    };
+                    let text_edit =
+                      Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                        range: *range,
+                        new_text: full_text.to_string(),
+                      }));
+                    let command = if key.name == last_key_name
+                      && !item.ends_with('/')
+                      && !specifier_exists(&item_specifier)
+                    {
+                      Some(lsp::Command {
+                        title: "".to_string(),
+                        command: "deno.cache".to_string(),
+                        arguments: Some(vec![
+                          json!([item_specifier]),
+                          json!(&resolved),
+                        ]),
+                      })
+                    } else {
+                      None
+                    };
+                    let detail = Some(format!("({})", key.name));
+                    let filter_text = Some(full_text.to_string());
+                    let sort_text = Some(format!("{:0>10}", idx + 1));
+                    let preselect =
+                      get_preselect(item.clone(), preselect.clone());
+                    let data = get_data_with_match(
+                      registry,
+                      &resolved,
+                      &tokens,
+                      &match_result,
+                      &key,
+                      &item,
+                    );
+                    let commit_characters = if is_incomplete {
+                      Some(
+                        REGISTRY_IMPORT_COMMIT_CHARS
+                          .iter()
+                          .map(|&c| c.into())
+                          .collect(),
+                      )
+                    } else {
+                      Some(
+                        IMPORT_COMMIT_CHARS.iter().map(|&c| c.into()).collect(),
+                      )
+                    };
+                    completions.insert(
+                      item,
+                      lsp::CompletionItem {
+                        label,
+                        kind,
+                        detail,
+                        sort_text,
+                        filter_text,
+                        text_edit,
+                        command,
+                        preselect,
+                        data,
+                        commit_characters,
+                        ..Default::default()
+                      },
+                    );
                   }
-                  None => (),
                 }
-                break;
               }
-              i -= 1;
-              // If we have fallen though to the first token, and we still
-              // didn't get a match
-              if i == 0 {
-                match &tokens[i] {
-                  // so if the first token is a string literal, we will return
-                  // that as a suggestion
-                  Token::String(s) => {
-                    if s.starts_with(path) {
-                      let label = s.to_string();
+            }
+            None => (),
+          }
+          break;
+        }
+        i -= 1;
+        // If we have fallen though to the first token, and we still
+        // didn't get a match
+        if i == 0 {
+          match &tokens[i] {
+            // so if the first token is a string literal, we will return
+            // that as a suggestion
+            Token::String(s) => {
+              if s.starts_with(path) {
+                let label = s.to_string();
+                let kind = Some(lsp::CompletionItemKind::FOLDER);
+                let mut url = resolved.as_ref().clone();
+                url.set_path(s);
+                let full_text = if let Some(suffix) =
+                  url.as_str().strip_prefix(resolved_str)
+                {
+                  format!("{text}{suffix}")
+                } else {
+                  url.to_string()
+                };
+                let text_edit =
+                  Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                    range: *range,
+                    new_text: full_text.to_string(),
+                  }));
+                let filter_text = Some(full_text.to_string());
+                completions.insert(
+                  s.to_string(),
+                  lsp::CompletionItem {
+                    label,
+                    kind,
+                    filter_text,
+                    sort_text: Some("1".to_string()),
+                    text_edit,
+                    preselect: Some(true),
+                    commit_characters: Some(
+                      REGISTRY_IMPORT_COMMIT_CHARS
+                        .iter()
+                        .map(|&c| c.into())
+                        .collect(),
+                    ),
+                    ..Default::default()
+                  },
+                );
+              }
+            }
+            // if the token though is a key, and the key has a prefix, and
+            // the path matches the prefix, we will go and get the items
+            // for that first key and return them.
+            Token::Key(k) => {
+              if let Some(prefix) = &k.prefix {
+                let maybe_url = registry.get_url_for_key(k);
+                if let Some(url) = maybe_url {
+                  if let Some(items) = self.get_items(url).await {
+                    let base = Url::parse(&origin).ok()?;
+                    let (items, preselect, incomplete) = match items {
+                      VariableItems::List(list) => {
+                        (list.items, list.preselect, list.is_incomplete)
+                      }
+                      VariableItems::Simple(items) => (items, None, false),
+                    };
+                    if incomplete {
+                      is_incomplete = true;
+                    }
+                    for (idx, item) in items.into_iter().enumerate() {
+                      let path = format!("{prefix}{item}");
                       let kind = Some(lsp::CompletionItemKind::FOLDER);
-                      let mut url = specifier.clone();
-                      url.set_path(s);
-                      let full_text = url.as_str();
+                      let item_specifier = base.join(&path).ok()?;
+                      let full_text = if let Some(suffix) =
+                        item_specifier.as_str().strip_prefix(resolved_str)
+                      {
+                        format!("{text}{suffix}")
+                      } else {
+                        item_specifier.to_string()
+                      };
                       let text_edit =
                         Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
                           range: *range,
-                          new_text: full_text.to_string(),
+                          new_text: full_text.clone(),
                         }));
+                      let command = if k.name == last_key_name
+                        && !specifier_exists(&item_specifier)
+                      {
+                        Some(lsp::Command {
+                          title: "".to_string(),
+                          command: "deno.cache".to_string(),
+                          arguments: Some(vec![
+                            json!([item_specifier]),
+                            json!(&resolved),
+                          ]),
+                        })
+                      } else {
+                        None
+                      };
+                      let detail = Some(format!("({})", k.name));
                       let filter_text = Some(full_text.to_string());
+                      let sort_text = Some(format!("{:0>10}", idx + 1));
+                      let preselect =
+                        get_preselect(item.clone(), preselect.clone());
+                      let data = get_data(registry, &resolved, k, &path);
+                      let commit_characters = if is_incomplete {
+                        Some(
+                          REGISTRY_IMPORT_COMMIT_CHARS
+                            .iter()
+                            .map(|&c| c.into())
+                            .collect(),
+                        )
+                      } else {
+                        Some(
+                          IMPORT_COMMIT_CHARS
+                            .iter()
+                            .map(|&c| c.into())
+                            .collect(),
+                        )
+                      };
                       completions.insert(
-                        s.to_string(),
+                        item.clone(),
                         lsp::CompletionItem {
-                          label,
+                          label: item,
                           kind,
+                          detail,
+                          sort_text,
                           filter_text,
-                          sort_text: Some("1".to_string()),
                           text_edit,
-                          preselect: Some(true),
-                          commit_characters: Some(
-                            REGISTRY_IMPORT_COMMIT_CHARS
-                              .iter()
-                              .map(|&c| c.into())
-                              .collect(),
-                          ),
+                          command,
+                          preselect,
+                          data,
+                          commit_characters,
                           ..Default::default()
                         },
                       );
                     }
                   }
-                  // if the token though is a key, and the key has a prefix, and
-                  // the path matches the prefix, we will go and get the items
-                  // for that first key and return them.
-                  Token::Key(k) => {
-                    if let Some(prefix) = &k.prefix {
-                      let maybe_url = registry.get_url_for_key(k);
-                      if let Some(url) = maybe_url {
-                        if let Some(items) = self.get_items(url).await {
-                          let base = Url::parse(&origin).ok()?;
-                          let (items, preselect, incomplete) = match items {
-                            VariableItems::List(list) => {
-                              (list.items, list.preselect, list.is_incomplete)
-                            }
-                            VariableItems::Simple(items) => {
-                              (items, None, false)
-                            }
-                          };
-                          if incomplete {
-                            is_incomplete = true;
-                          }
-                          for (idx, item) in items.into_iter().enumerate() {
-                            let path = format!("{prefix}{item}");
-                            let kind = Some(lsp::CompletionItemKind::FOLDER);
-                            let item_specifier = base.join(&path).ok()?;
-                            let full_text = item_specifier.as_str();
-                            let text_edit = Some(
-                              lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                                range: *range,
-                                new_text: full_text.to_string(),
-                              }),
-                            );
-                            let command = if k.name == last_key_name
-                              && !specifier_exists(&item_specifier)
-                            {
-                              Some(lsp::Command {
-                                title: "".to_string(),
-                                command: "deno.cache".to_string(),
-                                arguments: Some(vec![
-                                  json!([item_specifier]),
-                                  json!(&specifier),
-                                ]),
-                              })
-                            } else {
-                              None
-                            };
-                            let detail = Some(format!("({})", k.name));
-                            let filter_text = Some(full_text.to_string());
-                            let sort_text = Some(format!("{:0>10}", idx + 1));
-                            let preselect =
-                              get_preselect(item.clone(), preselect.clone());
-                            let data = get_data(registry, &specifier, k, &path);
-                            let commit_characters = if is_incomplete {
-                              Some(
-                                REGISTRY_IMPORT_COMMIT_CHARS
-                                  .iter()
-                                  .map(|&c| c.into())
-                                  .collect(),
-                              )
-                            } else {
-                              Some(
-                                IMPORT_COMMIT_CHARS
-                                  .iter()
-                                  .map(|&c| c.into())
-                                  .collect(),
-                              )
-                            };
-                            completions.insert(
-                              item.clone(),
-                              lsp::CompletionItem {
-                                label: item,
-                                kind,
-                                detail,
-                                sort_text,
-                                filter_text,
-                                text_edit,
-                                command,
-                                preselect,
-                                data,
-                                commit_characters,
-                                ..Default::default()
-                              },
-                            );
-                          }
-                        }
-                      }
-                    }
-                  }
                 }
-                break;
               }
             }
           }
-          // If we return None, other sources of completions will be looked for
-          // but if we did at least match part of a registry, we should send an
-          // empty vector so that no-completions will be sent back to the client
-          return if completions.is_empty() && !did_match {
-            None
-          } else {
-            Some(lsp::CompletionList {
-              items: completions.into_values().collect(),
-              is_incomplete,
-            })
-          };
+          break;
         }
       }
     }
-
-    self.get_origin_completions(current_specifier, range)
+    // If we return None, other sources of completions will be looked for
+    // but if we did at least match part of a registry, we should send an
+    // empty vector so that no-completions will be sent back to the client
+    if completions.is_empty() && !did_match {
+      None
+    } else {
+      Some(lsp::CompletionList {
+        items: completions.into_values().collect(),
+        is_incomplete,
+      })
+    }
   }
 
   pub async fn get_documentation(
@@ -1316,9 +1301,7 @@ mod tests {
         character: 21,
       },
     };
-    let completions = module_registry
-      .get_completions("h", 1, &range, |_| false)
-      .await;
+    let completions = module_registry.get_origin_completions("h", &range);
     assert!(completions.is_some());
     let completions = completions.unwrap().items;
     assert_eq!(completions.len(), 1);
@@ -1340,9 +1323,8 @@ mod tests {
         character: 36,
       },
     };
-    let completions = module_registry
-      .get_completions("http://localhost", 16, &range, |_| false)
-      .await;
+    let completions =
+      module_registry.get_origin_completions("http://localhost", &range);
     assert!(completions.is_some());
     let completions = completions.unwrap().items;
     assert_eq!(completions.len(), 1);
@@ -1377,7 +1359,7 @@ mod tests {
       },
     };
     let completions = module_registry
-      .get_completions("http://localhost:4545", 21, &range, |_| false)
+      .get_completions("http://localhost:4545", &range, None, |_| false)
       .await;
     assert!(completions.is_some());
     let completions = completions.unwrap().items;
@@ -1393,7 +1375,7 @@ mod tests {
       },
     };
     let completions = module_registry
-      .get_completions("http://localhost:4545/", 22, &range, |_| false)
+      .get_completions("http://localhost:4545/", &range, None, |_| false)
       .await;
     assert!(completions.is_some());
     let completions = completions.unwrap().items;
@@ -1409,7 +1391,7 @@ mod tests {
       },
     };
     let completions = module_registry
-      .get_completions("http://localhost:4545/x/", 24, &range, |_| false)
+      .get_completions("http://localhost:4545/x/", &range, None, |_| false)
       .await;
     assert!(completions.is_some());
     let completions = completions.unwrap();
@@ -1434,7 +1416,7 @@ mod tests {
       },
     };
     let completions = module_registry
-      .get_completions("http://localhost:4545/x/a", 25, &range, |_| false)
+      .get_completions("http://localhost:4545/x/a", &range, None, |_| false)
       .await;
     assert!(completions.is_some());
     let completions = completions.unwrap();
@@ -1470,7 +1452,7 @@ mod tests {
       },
     };
     let completions = module_registry
-      .get_completions("http://localhost:4545/x/a@", 26, &range, |_| false)
+      .get_completions("http://localhost:4545/x/a@", &range, None, |_| false)
       .await;
     assert!(completions.is_some());
     let completions = completions.unwrap().items;
@@ -1493,7 +1475,7 @@ mod tests {
       },
     };
     let completions = module_registry
-      .get_completions("http://localhost:4545/x/a@v1.", 29, &range, |_| false)
+      .get_completions("http://localhost:4545/x/a@v1.", &range, None, |_| false)
       .await;
     assert!(completions.is_some());
     let completions = completions.unwrap().items;
@@ -1516,9 +1498,12 @@ mod tests {
       },
     };
     let completions = module_registry
-      .get_completions("http://localhost:4545/x/a@v1.0.0/", 33, &range, |_| {
-        false
-      })
+      .get_completions(
+        "http://localhost:4545/x/a@v1.0.0/",
+        &range,
+        None,
+        |_| false,
+      )
       .await;
     assert!(completions.is_some());
     let completions = completions.unwrap().items;
@@ -1541,9 +1526,12 @@ mod tests {
       },
     };
     let completions = module_registry
-      .get_completions("http://localhost:4545/x/a@v1.0.0/b", 34, &range, |_| {
-        false
-      })
+      .get_completions(
+        "http://localhost:4545/x/a@v1.0.0/b",
+        &range,
+        None,
+        |_| false,
+      )
       .await;
     assert!(completions.is_some());
     let completions = completions.unwrap().items;
@@ -1565,8 +1553,8 @@ mod tests {
     let completions = module_registry
       .get_completions(
         "http://localhost:4545/x/a@v1.0.0/b/",
-        35,
         &range,
+        None,
         |_| false,
       )
       .await;
@@ -1602,7 +1590,7 @@ mod tests {
       },
     };
     let completions = module_registry
-      .get_completions("http://localhost:4545/", 22, &range, |_| false)
+      .get_completions("http://localhost:4545/", &range, None, |_| false)
       .await;
     assert!(completions.is_some());
     let completions = completions.unwrap().items;
@@ -1631,12 +1619,16 @@ mod tests {
       },
     };
     let completions = module_registry
-      .get_completions("http://localhost:4545/cde@", 26, &range, |_| false)
+      .get_completions("http://localhost:4545/cde@", &range, None, |_| false)
       .await;
     assert!(completions.is_some());
     let completions = completions.unwrap().items;
-    assert_eq!(completions.len(), 2);
     for completion in completions {
+      if let Some(filter_text) = completion.filter_text {
+        if !"http://localhost:4545/cde@".contains(&filter_text) {
+          continue;
+        }
+      }
       assert!(completion.text_edit.is_some());
       if let lsp::CompletionTextEdit::Edit(edit) = completion.text_edit.unwrap()
       {
@@ -1674,7 +1666,7 @@ mod tests {
       },
     };
     let completions = module_registry
-      .get_completions("http://localhost:4545/", 22, &range, |_| false)
+      .get_completions("http://localhost:4545/", &range, None, |_| false)
       .await;
     assert!(completions.is_some());
     let completions = completions.unwrap().items;
@@ -1693,6 +1685,48 @@ mod tests {
     }
   }
 
+  #[tokio::test]
+  async fn test_registry_completions_import_map() {
+    let _g = test_util::http_server();
+    let temp_dir = TempDir::new();
+    let location = temp_dir.path().join("registries").to_path_buf();
+    let mut module_registry = ModuleRegistry::new(
+      location,
+      Arc::new(HttpClientProvider::new(None, None)),
+    );
+    module_registry.enable("http://localhost:4545/").await;
+    let range = lsp::Range {
+      start: lsp::Position {
+        line: 0,
+        character: 20,
+      },
+      end: lsp::Position {
+        line: 0,
+        character: 33,
+      },
+    };
+    let completions = module_registry
+      .get_completions(
+        "localhost4545/",
+        &range,
+        Some(&ModuleSpecifier::parse("http://localhost:4545/").unwrap()),
+        |_| false,
+      )
+      .await;
+    assert!(completions.is_some());
+    let completions = completions.unwrap().items;
+    assert_eq!(completions.len(), 3);
+    for completion in completions {
+      assert!(completion.text_edit.is_some());
+      if let lsp::CompletionTextEdit::Edit(edit) = completion.text_edit.unwrap()
+      {
+        assert_eq!(edit.new_text, format!("localhost4545{}", completion.label));
+      } else {
+        unreachable!("unexpected text edit");
+      }
+    }
+  }
+
   #[test]
   fn test_parse_replacement_variables() {
     let actual = parse_replacement_variables(