Skip to content

Commit

Permalink
feat: support drag and drop of files into the typst editor (#635)
Browse files Browse the repository at this point in the history
* feat: support drag and drop of files into the typst editor

* feat: add configuration gate
  • Loading branch information
Myriad-Dreamin authored Oct 6, 2024
1 parent be9bf5e commit d6fae74
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 5 deletions.
10 changes: 10 additions & 0 deletions editors/vscode/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
17 changes: 12 additions & 5 deletions editors/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,20 @@ 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";
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<void> {
try {
Expand All @@ -64,13 +70,17 @@ export async function doActivate(context: ExtensionContext): Promise<void> {
// 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);
setClient(client);
// Activates features
labelFeatureActivate(context);
packageFeatureActivate(context);
if (extensionState.features.dragAndDrop) {
dragAndDropActivate(context);
}
if (extensionState.features.task) {
taskActivate(context);
}
Expand Down Expand Up @@ -115,10 +125,7 @@ function initClient(context: ExtensionContext, config: Record<string, any>) {
};

const clientOptions: LanguageClientOptions = {
documentSelector: [
{ scheme: "file", language: "typst" },
{ scheme: "untitled", language: "typst" },
],
documentSelector: typstDocumentSelector,
initializationOptions: config,
middleware: {
workspace: {
Expand Down
181 changes: 181 additions & 0 deletions editors/vscode/src/features/drag-and-drop.ts
Original file line number Diff line number Diff line change
@@ -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<string, ResourceKind> = {
".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<vscode.DocumentDropEdit | undefined> {
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;
}
}
2 changes: 2 additions & 0 deletions editors/vscode/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ interface ExtensionState {
features: {
task: boolean;
devKit: boolean;
dragAndDrop: boolean;
onEnter: boolean;
preview: boolean;
};
Expand All @@ -21,6 +22,7 @@ export const extensionState: ExtensionState = {
features: {
task: true,
devKit: false,
dragAndDrop: false,
onEnter: false,
preview: false,
},
Expand Down
5 changes: 5 additions & 0 deletions editors/vscode/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down

0 comments on commit d6fae74

Please sign in to comment.