mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-12-23 08:47:50 +00:00
Merge 2c61f06434 into 0bfe655ada
This commit is contained in:
commit
5f35e8b1cd
13 changed files with 296 additions and 14 deletions
|
|
@ -4,4 +4,4 @@ pub mod pack;
|
|||
pub use pack::*;
|
||||
|
||||
pub mod registry;
|
||||
pub use registry::{PackageError, PackageRegistry, PackageSpec};
|
||||
pub use registry::{PackageError, PackageRegistry, PackageSpec, PackageSpecExt};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use serde::Deserialize;
|
|||
use tinymist_std::time::UtcDateTime;
|
||||
pub use typst::diag::PackageError;
|
||||
pub use typst::syntax::package::PackageSpec;
|
||||
use typst::syntax::package::{PackageInfo, TemplateInfo};
|
||||
use typst::syntax::package::{PackageInfo, TemplateInfo, VersionlessPackageSpec};
|
||||
|
||||
mod dummy;
|
||||
pub use dummy::*;
|
||||
|
|
@ -30,6 +30,27 @@ pub use http::*;
|
|||
/// The default Typst registry.
|
||||
pub const DEFAULT_REGISTRY: &str = "https://packages.typst.org";
|
||||
|
||||
/// The namespace for Typst registry.
|
||||
pub const PREVIEW_NS: &str = "preview";
|
||||
|
||||
/// An extension trait for package specifications.
|
||||
pub trait PackageSpecExt {
|
||||
/// Returns true if the package spec is in the preview namespace.
|
||||
fn is_preview(&self) -> bool;
|
||||
}
|
||||
|
||||
impl PackageSpecExt for PackageSpec {
|
||||
fn is_preview(&self) -> bool {
|
||||
self.namespace == PREVIEW_NS
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageSpecExt for VersionlessPackageSpec {
|
||||
fn is_preview(&self) -> bool {
|
||||
self.namespace == PREVIEW_NS
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for package registries.
|
||||
pub trait PackageRegistry {
|
||||
/// A function to be called when the registry is reset.
|
||||
|
|
@ -85,6 +106,11 @@ impl PackageIndexEntry {
|
|||
version: self.package.version,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this entry matches a versionless package specification.
|
||||
pub fn matches_versionless(&self, spec: &VersionlessPackageSpec) -> bool {
|
||||
self.namespace == spec.namespace && self.package.name == spec.name
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_timestamp<'de, D>(deserializer: D) -> Result<Option<UtcDateTime>, D::Error>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use tinymist_std::ImmutPath;
|
|||
use typst::diag::{PackageResult, StrResult, eco_format};
|
||||
use typst::syntax::package::{PackageVersion, VersionlessPackageSpec};
|
||||
|
||||
use crate::registry::PackageIndexEntry;
|
||||
use crate::registry::{PREVIEW_NS, PackageIndexEntry, PackageSpecExt};
|
||||
|
||||
use super::{
|
||||
DEFAULT_REGISTRY, DummyNotifier, Notifier, PackageError, PackageRegistry, PackageSpec,
|
||||
|
|
@ -179,7 +179,7 @@ impl PackageStorage {
|
|||
}
|
||||
|
||||
// Download from network if it doesn't exist yet.
|
||||
if spec.namespace == "preview" {
|
||||
if spec.is_preview() {
|
||||
self.download_package(spec, &dir)?;
|
||||
if dir.exists() {
|
||||
return Ok(dir.into());
|
||||
|
|
@ -195,7 +195,7 @@ impl PackageStorage {
|
|||
&self,
|
||||
spec: &VersionlessPackageSpec,
|
||||
) -> StrResult<PackageVersion> {
|
||||
if spec.namespace == "preview" {
|
||||
if spec.is_preview() {
|
||||
// For `@preview`, download the package index and find the latest
|
||||
// version.
|
||||
self.download_index()
|
||||
|
|
@ -249,7 +249,7 @@ impl PackageStorage {
|
|||
}
|
||||
};
|
||||
for entry in &mut entries {
|
||||
entry.namespace = "preview".into();
|
||||
entry.namespace = PREVIEW_NS.into();
|
||||
}
|
||||
|
||||
entries
|
||||
|
|
@ -263,7 +263,7 @@ impl PackageStorage {
|
|||
/// # Panics
|
||||
/// Panics if the package spec namespace isn't `preview`.
|
||||
pub fn download_package(&self, spec: &PackageSpec, package_dir: &Path) -> PackageResult<()> {
|
||||
assert_eq!(spec.namespace, "preview");
|
||||
assert!(spec.is_preview(), "only preview packages can be downloaded");
|
||||
|
||||
let url = format!(
|
||||
"{DEFAULT_REGISTRY}/preview/{}-{}.tar.gz",
|
||||
|
|
|
|||
|
|
@ -431,7 +431,7 @@ struct ConvertResult {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tinymist_world::package::{PackageRegistry, PackageSpec};
|
||||
use tinymist_world::package::{PackageRegistry, PackageSpec, registry::PREVIEW_NS};
|
||||
|
||||
use super::{PackageInfo, package_docs, package_docs_md, package_docs_typ};
|
||||
use crate::tests::*;
|
||||
|
|
@ -471,7 +471,7 @@ mod tests {
|
|||
#[test]
|
||||
fn tidy() {
|
||||
test(PackageSpec {
|
||||
namespace: "preview".into(),
|
||||
namespace: PREVIEW_NS.into(),
|
||||
name: "tidy".into(),
|
||||
version: "0.3.0".parse().unwrap(),
|
||||
});
|
||||
|
|
@ -480,7 +480,7 @@ mod tests {
|
|||
#[test]
|
||||
fn touying() {
|
||||
test(PackageSpec {
|
||||
namespace: "preview".into(),
|
||||
namespace: PREVIEW_NS.into(),
|
||||
name: "touying".into(),
|
||||
version: "0.6.0".parse().unwrap(),
|
||||
});
|
||||
|
|
@ -489,7 +489,7 @@ mod tests {
|
|||
#[test]
|
||||
fn fletcher() {
|
||||
test(PackageSpec {
|
||||
namespace: "preview".into(),
|
||||
namespace: PREVIEW_NS.into(),
|
||||
name: "fletcher".into(),
|
||||
version: "0.5.8".parse().unwrap(),
|
||||
});
|
||||
|
|
@ -498,7 +498,7 @@ mod tests {
|
|||
#[test]
|
||||
fn cetz() {
|
||||
test(PackageSpec {
|
||||
namespace: "preview".into(),
|
||||
namespace: PREVIEW_NS.into(),
|
||||
name: "cetz".into(),
|
||||
version: "0.2.2".parse().unwrap(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
#import /* position after */ "@preview/example:0.1.1"
|
||||
|
|
@ -0,0 +1 @@
|
|||
#import /* position after */ "@preview/shiroa:0.2.3"
|
||||
|
|
@ -0,0 +1 @@
|
|||
#import /* position after */ "@preview/example:0.1.0"
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/hover.rs
|
||||
expression: content
|
||||
input_file: crates/tinymist-query/src/fixtures/hover/package_import_no_upgrade.typ
|
||||
---
|
||||
Range: 0:29:0:53
|
||||
|
||||
🌌 [Universe](https://typst.app/universe/package/example)
|
||||
|
||||
**Package:** `@preview/example:0.1.1`
|
||||
✅ **Up to date** (latest version)
|
||||
|
||||
**Description:** example package (mock).
|
||||
|
||||
**Available Versions (2)** (click to replace):
|
||||
- **0.1.1** /
|
||||
- [0.1.0](command:tinymist.replaceText?%7B%22range%22%3A%7B%22end%22%3A%7B%22character%22%3A53%2C%22line%22%3A0%7D%2C%22start%22%3A%7B%22character%22%3A29%2C%22line%22%3A0%7D%7D%2C%22replace%22%3A%22%5C%22%40preview%2Fexample%3A0%2E1%2E0%5C%22%22%7D) /
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/hover.rs
|
||||
expression: content
|
||||
input_file: crates/tinymist-query/src/fixtures/hover/package_import_not_found.typ
|
||||
---
|
||||
Range: 0:29:0:52
|
||||
|
||||
🌌 [Universe](https://typst.app/universe/package/shiroa)
|
||||
|
||||
**Package:** `@preview/shiroa:0.2.3`
|
||||
‼️ **Version 0.2.3 not found**
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
source: crates/tinymist-query/src/hover.rs
|
||||
expression: content
|
||||
input_file: crates/tinymist-query/src/fixtures/hover/package_import_upgrade.typ
|
||||
---
|
||||
Range: 0:29:0:53
|
||||
|
||||
🌌 [Universe](https://typst.app/universe/package/example)
|
||||
|
||||
**Package:** `@preview/example:0.1.0`
|
||||
⚠️ **Newer version available: 0.1.1**
|
||||
|
||||
**Description:** example package (mock).
|
||||
|
||||
**Available Versions (2)** (click to replace):
|
||||
- [0.1.1](command:tinymist.replaceText?%7B%22range%22%3A%7B%22end%22%3A%7B%22character%22%3A53%2C%22line%22%3A0%7D%2C%22start%22%3A%7B%22character%22%3A29%2C%22line%22%3A0%7D%7D%2C%22replace%22%3A%22%5C%22%40preview%2Fexample%3A0%2E1%2E1%5C%22%22%7D) /
|
||||
- **0.1.0** /
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
use core::fmt::{self, Write};
|
||||
use std::cmp::Reverse;
|
||||
use std::str::FromStr;
|
||||
|
||||
use tinymist_std::typst::TypstDocument;
|
||||
use tinymist_world::package::{PackageSpec, PackageSpecExt};
|
||||
use typst::foundations::repr::separated_list;
|
||||
use typst_shim::syntax::LinkedNodeExt;
|
||||
|
||||
|
|
@ -100,6 +103,7 @@ impl HoverWorker<'_> {
|
|||
let source = self.source.clone();
|
||||
let leaf = LinkedNode::new(source.root()).leaf_at_compat(self.cursor)?;
|
||||
|
||||
self.package_import(&leaf);
|
||||
self.definition(&leaf)
|
||||
.or_else(|| self.star(&leaf))
|
||||
.or_else(|| self.link(&leaf))
|
||||
|
|
@ -214,6 +218,195 @@ impl HoverWorker<'_> {
|
|||
Some(())
|
||||
}
|
||||
|
||||
fn package_import(&mut self, node: &LinkedNode) -> Option<()> {
|
||||
// Check if we're in a string literal that's part of an import
|
||||
if !matches!(node.kind(), SyntaxKind::Str) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Navigate up to find the ModuleImport node
|
||||
let import_node = node.parent()?.cast::<ast::ModuleImport>()?;
|
||||
|
||||
// Check if this is a package import
|
||||
if let ast::Expr::Str(str_node) = import_node.source()
|
||||
&& let import_str = str_node.get()
|
||||
&& import_str.starts_with("@")
|
||||
&& let Ok(package_spec) = PackageSpec::from_str(&import_str)
|
||||
{
|
||||
self.def
|
||||
.push(self.get_package_hover_info(&package_spec, node));
|
||||
return Some(());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Get package information for hover content
|
||||
fn get_package_hover_info(
|
||||
&self,
|
||||
package_spec: &PackageSpec,
|
||||
import_str_node: &LinkedNode,
|
||||
) -> String {
|
||||
let versionless_spec = package_spec.versionless();
|
||||
|
||||
// Get all matching packages
|
||||
let w = self.ctx.world().clone();
|
||||
let mut packages = vec![];
|
||||
if package_spec.is_preview() {
|
||||
packages.extend(
|
||||
w.packages()
|
||||
.iter()
|
||||
.filter(|it| it.matches_versionless(&versionless_spec)),
|
||||
);
|
||||
}
|
||||
// Add non-preview packages
|
||||
#[cfg(feature = "local-registry")]
|
||||
let local_packages = self.ctx.non_preview_packages();
|
||||
#[cfg(feature = "local-registry")]
|
||||
if !package_spec.is_preview() {
|
||||
packages.extend(
|
||||
local_packages
|
||||
.iter()
|
||||
.filter(|it| it.matches_versionless(&versionless_spec)),
|
||||
);
|
||||
}
|
||||
|
||||
// Sort by version descending
|
||||
packages.sort_by_key(|entry| Reverse(entry.package.version));
|
||||
|
||||
let current_entry = packages
|
||||
.iter()
|
||||
.find(|entry| entry.package.version == package_spec.version);
|
||||
|
||||
let mut info = String::new();
|
||||
{
|
||||
// Add links
|
||||
let mut links_line = Vec::new();
|
||||
|
||||
if package_spec.is_preview() {
|
||||
let package_name = &package_spec.name;
|
||||
|
||||
// Universe page
|
||||
let universe_url = format!("https://typst.app/universe/package/{package_name}");
|
||||
links_line.push(format!("🌌 [Universe]({universe_url})"));
|
||||
}
|
||||
|
||||
if let Some(current_entry) = current_entry {
|
||||
// Repository URL
|
||||
if let Some(ref repo) = current_entry.package.repository {
|
||||
links_line.push(format!("🔗 [Repository]({repo})"));
|
||||
}
|
||||
|
||||
// Homepage URL
|
||||
if let Some(ref homepage) = current_entry.package.homepage {
|
||||
links_line.push(format!("🏠 [Homepage]({homepage})"));
|
||||
}
|
||||
}
|
||||
|
||||
if !links_line.is_empty() {
|
||||
info.push_str(&links_line.iter().join(" | "));
|
||||
info.push_str("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Package header
|
||||
if !package_spec.is_preview() {
|
||||
info.push_str("ℹ️ This is a local package\n\n");
|
||||
}
|
||||
|
||||
info.push_str(&format!("**Package:** `{package_spec}`\n"));
|
||||
// Check version information and show status
|
||||
if current_entry.is_none() {
|
||||
info.push_str(&format!(
|
||||
"‼️ **Version {} not found**\n\n",
|
||||
package_spec.version
|
||||
));
|
||||
} else if let Some(latest) = packages.first() {
|
||||
let latest_version = &latest.package.version;
|
||||
if *latest_version != package_spec.version {
|
||||
info.push_str(&format!(
|
||||
"⚠️ **Newer version available: {latest_version}**\n"
|
||||
));
|
||||
} else {
|
||||
info.push_str("✅ **Up to date** (latest version)\n");
|
||||
}
|
||||
}
|
||||
info.push('\n');
|
||||
|
||||
let date_format = tinymist_std::time::yyyy_mm_dd();
|
||||
|
||||
// Add manifest information if available
|
||||
if let Some(current_entry) = current_entry {
|
||||
let pkg_info = ¤t_entry.package;
|
||||
|
||||
if !pkg_info.authors.is_empty() {
|
||||
info.push_str(&format!("**Authors:** {}\n\n", pkg_info.authors.join(", ")));
|
||||
}
|
||||
|
||||
if let Some(description) = pkg_info.description.as_ref() {
|
||||
info.push_str(&format!("**Description:** {description}\n\n"));
|
||||
}
|
||||
|
||||
if let Some(license) = &pkg_info.license {
|
||||
info.push_str(&format!("**License:** {license}\n\n"));
|
||||
}
|
||||
|
||||
if let Some(updated_at) = ¤t_entry.updated_at {
|
||||
info.push_str(&format!(
|
||||
"**Updated:** {}\n\n",
|
||||
updated_at
|
||||
.format(&date_format)
|
||||
.unwrap_or_else(|_| "unknown".to_string())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Show version history for preview packages
|
||||
if !packages.is_empty() {
|
||||
info.push_str(&format!(
|
||||
"**Available Versions ({})** (click to replace):\n",
|
||||
packages.len()
|
||||
));
|
||||
for entry in &packages {
|
||||
let version = &entry.package.version;
|
||||
let release_date = entry
|
||||
.updated_at
|
||||
.and_then(|time| time.format(&date_format).ok())
|
||||
.unwrap_or_default();
|
||||
if *version == package_spec.version {
|
||||
// Current version
|
||||
info.push_str(&format!("- **{version}** / {release_date}\n"));
|
||||
continue;
|
||||
}
|
||||
// Other versions
|
||||
let lsp_range = self.ctx.to_lsp_range(import_str_node.range(), &self.source);
|
||||
let args = serde_json::json!({
|
||||
"range": lsp_range,
|
||||
"replace": format!(
|
||||
"\"@{}/{}:{}\"",
|
||||
package_spec.namespace, package_spec.name, version
|
||||
)
|
||||
});
|
||||
let json_str = match serde_json::to_string(&args) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("Failed to serialize arguments for replaceText command: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let encoded = percent_encoding::utf8_percent_encode(
|
||||
&json_str,
|
||||
percent_encoding::NON_ALPHANUMERIC,
|
||||
);
|
||||
let version_url = format!("command:tinymist.replaceText?{encoded}");
|
||||
info.push_str(&format!("- [{version}]({version_url}) / {release_date}\n"));
|
||||
}
|
||||
info.push('\n');
|
||||
}
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
fn link(&mut self, mut node: &LinkedNode) -> Option<()> {
|
||||
while !matches!(node.kind(), SyntaxKind::FuncCall) {
|
||||
node = node.parent()?;
|
||||
|
|
|
|||
|
|
@ -232,8 +232,9 @@ async function languageActivate(context: IContext) {
|
|||
// We would like to define it at the server side, but it is not possible for now.
|
||||
// https://github.com/microsoft/language-server-protocol/issues/1117
|
||||
commands.registerCommand("tinymist.triggerSuggestAndParameterHints", triggerSuggestAndParameterHints),
|
||||
|
||||
commands.registerTextEditorCommand("tinymist.replaceText", commandReplaceText),
|
||||
);
|
||||
// context.subscriptions.push
|
||||
const provider = new SymbolViewProvider(context.context);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider(SymbolViewProvider.Name, provider),
|
||||
|
|
@ -639,3 +640,17 @@ function triggerSuggestAndParameterHints() {
|
|||
vscode.commands.executeCommand("editor.action.triggerSuggest");
|
||||
vscode.commands.executeCommand("editor.action.triggerParameterHints");
|
||||
}
|
||||
|
||||
async function commandReplaceText(
|
||||
editor: TextEditor,
|
||||
edit: vscode.TextEditorEdit,
|
||||
args?: { range: vscode.Range; replace: string },
|
||||
): Promise<void> {
|
||||
if (editor && args) {
|
||||
const range = new vscode.Range(
|
||||
new vscode.Position(args.range.start.line, args.range.start.character),
|
||||
new vscode.Position(args.range.end.line, args.range.end.character),
|
||||
);
|
||||
edit.replace(range, args.replace);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ export class LanguageState {
|
|||
};
|
||||
|
||||
const trustedCommands = {
|
||||
enabledCommands: ["tinymist.openInternal", "tinymist.openExternal"],
|
||||
enabledCommands: ["tinymist.openInternal", "tinymist.openExternal", "tinymist.replaceText"],
|
||||
};
|
||||
const hoverStorage =
|
||||
extensionState.features.renderDocs && LanguageState.HoverTmpStorage
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue