Skip to content

Commit

Permalink
Add drop/paste resource for css (#221612)
Browse files Browse the repository at this point in the history
* New drop and paste providers that create a url function snippet

* added url pasting feature

* added url pasting feature

* added url pasting feature

* Target just dropping/pasting resources for now

* Move files

* Remove unused strings

* Removing more unused logic for now

* Remove tsconfig change

* Remove doc file

* Capitalize

* Remove old proposal names

---------

Co-authored-by: Meghan Kulkarni <[email protected]>
Co-authored-by: Martin Aeschlimann <[email protected]>
  • Loading branch information
3 people authored Jul 16, 2024
1 parent 06f8ef1 commit 8d40a80
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ExtensionContext, Uri, l10n } from 'vscode';
import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient';
import { startClient, LanguageClientConstructor } from '../cssClient';
import { LanguageClient } from 'vscode-languageclient/browser';
import { registerDropOrPasteResourceSupport } from '../dropOrPaste/dropOrPasteResource';

let client: BaseLanguageClient | undefined;

Expand All @@ -23,6 +24,7 @@ export async function activate(context: ExtensionContext) {

client = await startClient(context, newLanguageClient, { TextDecoder });

context.subscriptions.push(registerDropOrPasteResourceSupport({ language: 'css', scheme: '*' }));
} catch (e) {
console.log(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as path from 'path';
import * as vscode from 'vscode';
import { getDocumentDir, Mimes, Schemes } from './shared';
import { UriList } from './uriList';

class DropOrPasteResourceProvider implements vscode.DocumentDropEditProvider, vscode.DocumentPasteEditProvider {
readonly kind = vscode.DocumentDropOrPasteEditKind.Empty.append('css', 'url');

async provideDocumentDropEdits(
document: vscode.TextDocument,
position: vscode.Position,
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken,
): Promise<vscode.DocumentDropEdit | undefined> {
const uriList = await this.getUriList(dataTransfer);
if (!uriList.entries.length || token.isCancellationRequested) {
return;
}

const snippet = await this.createUriListSnippet(uriList);
if (!snippet || token.isCancellationRequested) {
return;
}

return {
kind: this.kind,
title: snippet.label,
insertText: snippet.snippet.value,
yieldTo: this.pasteAsCssUrlByDefault(document, position) ? [] : [vscode.DocumentDropOrPasteEditKind.Empty.append('uri')]
};
}

async provideDocumentPasteEdits(
document: vscode.TextDocument,
ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
_context: vscode.DocumentPasteEditContext,
token: vscode.CancellationToken
): Promise<vscode.DocumentPasteEdit[] | undefined> {
const uriList = await this.getUriList(dataTransfer);
if (!uriList.entries.length || token.isCancellationRequested) {
return;
}

const snippet = await this.createUriListSnippet(uriList);
if (!snippet || token.isCancellationRequested) {
return;
}

return [{
kind: this.kind,
title: snippet.label,
insertText: snippet.snippet.value,
yieldTo: this.pasteAsCssUrlByDefault(document, ranges[0].start) ? [] : [vscode.DocumentDropOrPasteEditKind.Empty.append('uri')]
}];
}

private async getUriList(dataTransfer: vscode.DataTransfer): Promise<UriList> {
const urlList = await dataTransfer.get(Mimes.uriList)?.asString();
if (urlList) {
return UriList.from(urlList);
}

// Find file entries
const uris: vscode.Uri[] = [];
for (const [_, entry] of dataTransfer) {
const file = entry.asFile();
if (file?.uri) {
uris.push(file.uri);
}
}

return new UriList(uris.map(uri => ({ uri, str: uri.toString(true) })));
}

private async createUriListSnippet(uriList: UriList): Promise<{ readonly snippet: vscode.SnippetString; readonly label: string } | undefined> {
if (!uriList.entries.length) {
return;
}

const snippet = new vscode.SnippetString();
for (let i = 0; i < uriList.entries.length; i++) {
const uri = uriList.entries[i];
const relativePath = getRelativePath(uri.uri);
const urlText = relativePath ?? uri.str;

snippet.appendText(`url(${urlText})`);
if (i !== uriList.entries.length - 1) {
snippet.appendText(' ');
}
}

return {
snippet,
label: uriList.entries.length > 1
? vscode.l10n.t('Insert url() Functions')
: vscode.l10n.t('Insert url() Function')
};
}

private pasteAsCssUrlByDefault(document: vscode.TextDocument, position: vscode.Position): boolean {
const regex = /url\(.+?\)/gi;
for (const match of Array.from(document.lineAt(position.line).text.matchAll(regex))) {
if (position.character > match.index && position.character < match.index + match[0].length) {
return false;
}
}
return true;
}
}

function getRelativePath(file: vscode.Uri): string | undefined {
const dir = getDocumentDir(file);
if (dir && dir.scheme === file.scheme && dir.authority === file.authority) {
if (file.scheme === Schemes.file) {
// On windows, we must use the native `path.relative` to generate the relative path
// so that drive-letters are resolved cast insensitively. However we then want to
// convert back to a posix path to insert in to the document
const relativePath = path.relative(dir.fsPath, file.fsPath);
return path.posix.normalize(relativePath.split(path.sep).join(path.posix.sep));
}

return path.posix.relative(dir.path, file.path);
}

return undefined;
}

export function registerDropOrPasteResourceSupport(selector: vscode.DocumentSelector): vscode.Disposable {
const provider = new DropOrPasteResourceProvider();

return vscode.Disposable.from(
vscode.languages.registerDocumentDropEditProvider(selector, provider, {
providedDropEditKinds: [provider.kind],
dropMimeTypes: [
Mimes.uriList,
'files'
]
}),
vscode.languages.registerDocumentPasteEditProvider(selector, provider, {
providedPasteEditKinds: [provider.kind],
pasteMimeTypes: [
Mimes.uriList,
'files'
]
})
);
}
42 changes: 42 additions & 0 deletions extensions/css-language-features/client/src/dropOrPaste/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { Utils } from 'vscode-uri';

export const Schemes = Object.freeze({
file: 'file',
notebookCell: 'vscode-notebook-cell',
untitled: 'untitled',
});

export const Mimes = Object.freeze({
plain: 'text/plain',
uriList: 'text/uri-list',
});


export function getDocumentDir(uri: vscode.Uri): vscode.Uri | undefined {
const docUri = getParentDocumentUri(uri);
if (docUri.scheme === Schemes.untitled) {
return vscode.workspace.workspaceFolders?.[0]?.uri;
}
return Utils.dirname(docUri);
}

function getParentDocumentUri(uri: vscode.Uri): vscode.Uri {
if (uri.scheme === Schemes.notebookCell) {
// is notebook documents necessary?
for (const notebook of vscode.workspace.notebookDocuments) {
for (const cell of notebook.getCells()) {
if (cell.document.uri.toString() === uri.toString()) {
return notebook.uri;
}
}
}
}

return uri;
}
38 changes: 38 additions & 0 deletions extensions/css-language-features/client/src/dropOrPaste/uriList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';

function splitUriList(str: string): string[] {
return str.split('\r\n');
}

function parseUriList(str: string): string[] {
return splitUriList(str)
.filter(value => !value.startsWith('#')) // Remove comments
.map(value => value.trim());
}

export class UriList {

static from(str: string): UriList {
return new UriList(coalesce(parseUriList(str).map(line => {
try {
return { uri: vscode.Uri.parse(line), str: line };
} catch {
// Uri parse failure
return undefined;
}
})));
}

constructor(
public readonly entries: ReadonlyArray<{ readonly uri: vscode.Uri; readonly str: string }>
) { }
}

function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
return <T[]>array.filter(e => !!e);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { getNodeFSRequestService } from './nodeFs';
import { ExtensionContext, extensions, l10n } from 'vscode';
import { startClient, LanguageClientConstructor } from '../cssClient';
import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node';
import { TextDecoder } from 'util';

import { ExtensionContext, extensions, l10n } from 'vscode';
import { BaseLanguageClient, LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient/node';
import { LanguageClientConstructor, startClient } from '../cssClient';
import { getNodeFSRequestService } from './nodeFs';
import { registerDropOrPasteResourceSupport } from '../dropOrPaste/dropOrPasteResource';

let client: BaseLanguageClient | undefined;

Expand Down Expand Up @@ -37,6 +37,8 @@ export async function activate(context: ExtensionContext) {
process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? '';

client = await startClient(context, newLanguageClient, { fs: getNodeFSRequestService(), TextDecoder });

context.subscriptions.push(registerDropOrPasteResourceSupport({ language: 'css', scheme: '*' }));
}

export async function deactivate(): Promise<void> {
Expand All @@ -45,3 +47,4 @@ export async function deactivate(): Promise<void> {
client = undefined;
}
}

3 changes: 2 additions & 1 deletion extensions/css-language-features/client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
},
"include": [
"src/**/*",
"../../../src/vscode-dts/vscode.d.ts"
"../../../src/vscode-dts/vscode.d.ts",
"../../../src/vscode-dts/vscode.proposed.documentPaste.d.ts"
]
}
3 changes: 3 additions & 0 deletions extensions/css-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"supported": true
}
},
"enabledApiProposals": [
"documentPaste"
],
"scripts": {
"compile": "npx gulp compile-extension:css-language-features-client compile-extension:css-language-features-server",
"watch": "npx gulp watch-extension:css-language-features-client watch-extension:css-language-features-server",
Expand Down

0 comments on commit 8d40a80

Please sign in to comment.