From d6fae7477394f87b03b431b70693e02d1c492190 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:57:54 +0800 Subject: [PATCH] feat: support drag and drop of files into the typst editor (#635) * feat: support drag and drop of files into the typst editor * feat: add configuration gate --- editors/vscode/Configuration.md | 10 + editors/vscode/package.json | 10 + editors/vscode/src/extension.ts | 17 +- editors/vscode/src/features/drag-and-drop.ts | 181 +++++++++++++++++++ editors/vscode/src/state.ts | 2 + editors/vscode/src/util.ts | 5 + 6 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 editors/vscode/src/features/drag-and-drop.ts diff --git a/editors/vscode/Configuration.md b/editors/vscode/Configuration.md index 2ea4be0a3..466783589 100644 --- a/editors/vscode/Configuration.md +++ b/editors/vscode/Configuration.md @@ -106,6 +106,16 @@ Set the print width for the formatter, which is a **soft limit** of characters p - **Type**: `number` - **Default**: `120` +## `tinymist.dragAndDrop` + +Whether to handle drag-and-drop of resources into the editing typst document. + +- **Type**: `boolean` +- **Enum**: + - `enable` + - `disable` +- **Default**: `"enable"` + ## `tinymist.previewFeature` Enable or disable preview features of Typst. Note: restarting the editor is required to change this setting. diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 3ff8f947c..e0d2b100d 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -396,6 +396,16 @@ "type": "number", "default": 120 }, + "tinymist.dragAndDrop": { + "title": "Drag and drop", + "description": "Whether to handle drag-and-drop of resources into the editing typst document. Note: restarting the editor is required to change this setting.", + "type": "string", + "default": "enable", + "enum": [ + "enable", + "disable" + ] + }, "tinymist.previewFeature": { "title": "Enable preview features", "description": "Enable or disable preview features of Typst. Note: restarting the editor is required to change this setting.", diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index 218a2f355..dd804d7f8 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -32,7 +32,12 @@ import { previewProcessOutline, } from "./features/preview"; import { commandCreateLocalPackage, commandOpenLocalPackage } from "./package-manager"; -import { activeTypstEditor, DisposeList, getSensibleTextEditorColumn } from "./util"; +import { + activeTypstEditor, + DisposeList, + getSensibleTextEditorColumn, + typstDocumentSelector, +} from "./util"; import { client, getClient, setClient, tinymist } from "./lsp"; import { taskActivate } from "./features/tasks"; import { onEnterHandler } from "./lsp.on-enter"; @@ -40,6 +45,7 @@ import { extensionState } from "./state"; import { devKitFeatureActivate } from "./features/dev-kit"; import { labelFeatureActivate } from "./features/label"; import { packageFeatureActivate } from "./features/package"; +import { dragAndDropActivate } from "./features/drag-and-drop"; export async function activate(context: ExtensionContext): Promise { try { @@ -64,6 +70,7 @@ export async function doActivate(context: ExtensionContext): Promise { // Sets features extensionState.features.preview = config.previewFeature === "enable"; extensionState.features.devKit = isDevMode || config.devKit === "enable"; + extensionState.features.dragAndDrop = config.dragAndDrop === "enable"; extensionState.features.onEnter = !!config.onEnterEvent; // Initializes language client const client = initClient(context, config); @@ -71,6 +78,9 @@ export async function doActivate(context: ExtensionContext): Promise { // Activates features labelFeatureActivate(context); packageFeatureActivate(context); + if (extensionState.features.dragAndDrop) { + dragAndDropActivate(context); + } if (extensionState.features.task) { taskActivate(context); } @@ -115,10 +125,7 @@ function initClient(context: ExtensionContext, config: Record) { }; const clientOptions: LanguageClientOptions = { - documentSelector: [ - { scheme: "file", language: "typst" }, - { scheme: "untitled", language: "typst" }, - ], + documentSelector: typstDocumentSelector, initializationOptions: config, middleware: { workspace: { diff --git a/editors/vscode/src/features/drag-and-drop.ts b/editors/vscode/src/features/drag-and-drop.ts new file mode 100644 index 000000000..8354a399a --- /dev/null +++ b/editors/vscode/src/features/drag-and-drop.ts @@ -0,0 +1,181 @@ +import * as vscode from "vscode"; +import { dirname, extname, relative } from "path"; +import { typstDocumentSelector } from "../util"; + +export function dragAndDropActivate(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.languages.registerDocumentDropEditProvider(typstDocumentSelector, new TextProvider()), + ); +} + +enum ResourceKind { + BuiltinImage, + Webp, + Source, + Markdown, + TeX, + Json, + Toml, + Csv, + Yaml, + Bib, +} + +const resourceKinds: Record = { + ".jpg": ResourceKind.BuiltinImage, + ".jpeg": ResourceKind.BuiltinImage, + ".png": ResourceKind.BuiltinImage, + ".gif": ResourceKind.BuiltinImage, + ".bmp": ResourceKind.BuiltinImage, + ".ico": ResourceKind.BuiltinImage, + ".svg": ResourceKind.BuiltinImage, + ".webp": ResourceKind.Webp, + ".typst": ResourceKind.Source, + ".typ": ResourceKind.Source, + ".md": ResourceKind.Markdown, + ".tex": ResourceKind.TeX, + ".json": ResourceKind.Json, + ".jsonc": ResourceKind.Json, + ".json5": ResourceKind.Json, + ".toml": ResourceKind.Toml, + ".csv": ResourceKind.Csv, + ".yaml": ResourceKind.Yaml, + ".yml": ResourceKind.Yaml, + ".bib": ResourceKind.Bib, +}; + +export class TextProvider implements vscode.DocumentDropEditProvider { + async provideDocumentDropEdits( + doc: vscode.TextDocument, + position: vscode.Position, + dataTransfer: vscode.DataTransfer, + token: vscode.CancellationToken, + ): Promise { + const plainText = dataTransfer.get("text/plain"); + if (!plainText) { + return; + } + + const dropFileUri = doc.uri; + const dragFileUri = vscode.Uri.parse(plainText.value); + + let dragFilePath = ""; + let workspaceFolder = vscode.workspace.getWorkspaceFolder(dragFileUri); + if (dropFileUri.scheme === "untitled") { + if (workspaceFolder) { + dragFilePath = relative(workspaceFolder.uri.fsPath, dragFileUri.fsPath); + } + } else { + dragFilePath = relative(dirname(dropFileUri.fsPath), dragFileUri.fsPath); + } + + let barPath = dragFilePath.replace(/\\/g, "/"); + let strPath = `"${barPath}"`; + let codeSnippet = strPath; + let resourceKind: ResourceKind | undefined = resourceKinds[extname(dragFileUri.fsPath)]; + // todo: fetch latest version + const additionalPkgs: [string, string, string | undefined][] = []; + switch (resourceKind) { + case ResourceKind.BuiltinImage: + codeSnippet = `image(${strPath})`; + break; + case ResourceKind.Webp: + additionalPkgs.push(["@preview/grayness", "0.1.0", "grayscale-image"]); + codeSnippet = `grayscale-image(read(${strPath}))`; + break; + case ResourceKind.Source: + codeSnippet = `include ${strPath}`; + break; + case ResourceKind.Markdown: + additionalPkgs.push(["@preview/cmarker", "0.1.1", undefined]); + codeSnippet = `cmarker.render(read(${strPath}))`; + break; + case ResourceKind.TeX: + additionalPkgs.push(["@preview/mitex", "0.2.4", "mitex"]); + codeSnippet = `mitex(read(${strPath}))`; + break; + case ResourceKind.Json: + codeSnippet = `json(${strPath})`; + break; + case ResourceKind.Toml: + codeSnippet = `toml(${strPath})`; + break; + case ResourceKind.Csv: + codeSnippet = `csv(${strPath})`; + break; + case ResourceKind.Yaml: + codeSnippet = `yaml(${strPath})`; + break; + case ResourceKind.Bib: + codeSnippet = `bibliography(${strPath})`; + break; + default: + codeSnippet = `read(${strPath})`; + break; + } + + const res = await vscode.commands.executeCommand< + [{ mode: "math" | "markup" | "code" | "comment" | "string" | "raw" }] + >("tinymist.interactCodeContext", { + textDocument: { + uri: doc.uri.toString(), + }, + query: [ + { + kind: "modeAt", + position: { + line: position.line, + character: position.character, + }, + }, + ], + }); + + let text = codeSnippet; + switch (res?.[0]?.mode) { + case "math": + case "markup": + text = `#${codeSnippet}`; + break; + case "code": + text = codeSnippet; + break; + case "comment": + case "raw": + case "string": + text = barPath; + break; + } + + let additionalEdit = undefined; + if (additionalPkgs.length > 0) { + additionalEdit = new vscode.WorkspaceEdit(); + const t = doc.getText(); + for (const [pkgName, version, importName] of additionalPkgs) { + if (!t.includes(pkgName)) { + if (importName) { + additionalEdit.insert( + doc.uri, + new vscode.Position(0, 0), + `#import "${pkgName}:${version}": ${importName}\n`, + ); + } else { + additionalEdit.insert( + doc.uri, + new vscode.Position(0, 0), + `#import "${pkgName}:${version}"\n`, + ); + } + } + } + } + + // console.log(resourceKind, res?.[0]?.mode, codeSnippet, text); + + const insertText = new vscode.SnippetString(text); + const edit = new vscode.DocumentDropEdit(insertText); + edit.additionalEdit = additionalEdit; + + return edit; + } +} diff --git a/editors/vscode/src/state.ts b/editors/vscode/src/state.ts index b2a923f90..6b92d57b5 100644 --- a/editors/vscode/src/state.ts +++ b/editors/vscode/src/state.ts @@ -6,6 +6,7 @@ interface ExtensionState { features: { task: boolean; devKit: boolean; + dragAndDrop: boolean; onEnter: boolean; preview: boolean; }; @@ -21,6 +22,7 @@ export const extensionState: ExtensionState = { features: { task: true, devKit: false, + dragAndDrop: false, onEnter: false, preview: false, }, diff --git a/editors/vscode/src/util.ts b/editors/vscode/src/util.ts index 91ed6290f..8b136f71f 100644 --- a/editors/vscode/src/util.ts +++ b/editors/vscode/src/util.ts @@ -3,6 +3,11 @@ import * as path from "path"; import { ViewColumn } from "vscode"; import { readFile } from "fs/promises"; +export const typstDocumentSelector = [ + { scheme: "file", language: "typst" }, + { scheme: "untitled", language: "typst" }, +]; + export function activeTypstEditor() { const editor = vscode.window.activeTextEditor; if (!editor || editor.document.languageId !== "typst") {