Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add drop feedback UX #179434

Merged
merged 1 commit into from Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion extensions/markdown-language-features/package.json
Expand Up @@ -16,7 +16,8 @@
"Programming Languages"
],
"enabledApiProposals": [
"documentPaste"
"documentPaste",
"dropMetadata"
],
"activationEvents": [
"onLanguage:markdown",
Expand Down
Expand Up @@ -45,7 +45,7 @@ class PasteEditProvider implements vscode.DocumentPasteEditProvider {
}

const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
return snippet ? new vscode.DocumentPasteEdit(snippet) : undefined;
return snippet ? new vscode.DocumentPasteEdit(snippet.snippet) : undefined;
}

private async _makeCreateImagePasteEdit(document: vscode.TextDocument, file: vscode.DataTransferFile, token: vscode.CancellationToken): Promise<vscode.DocumentPasteEdit | undefined> {
Expand Down
Expand Up @@ -38,12 +38,23 @@ export function registerDropIntoEditorSupport(selector: vscode.DocumentSelector)
}

const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
return snippet ? new vscode.DocumentDropEdit(snippet) : undefined;
if (!snippet) {
return undefined;
}

const edit = new vscode.DocumentDropEdit(snippet.snippet);
edit.label = snippet.label;
return edit;
}
}, {
id: 'vscode.markdown.insertLink',
dropMimeTypes: [
'text/uri-list'
]
});
}

export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.SnippetString | undefined> {
export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<{ snippet: vscode.SnippetString; label: string } | undefined> {
const urlList = await dataTransfer.get('text/uri-list')?.asString();
if (!urlList || token.isCancellationRequested) {
return undefined;
Expand All @@ -58,7 +69,17 @@ export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTr
}
}

return createUriListSnippet(document, uris);
const snippet = createUriListSnippet(document, uris);
if (!snippet) {
return undefined;
}

return {
snippet: snippet,
label: uris.length > 1
? vscode.l10n.t('Insert uri links')
: vscode.l10n.t('Insert uri link')
};
}

interface UriListSnippetOptions {
Expand Down
3 changes: 2 additions & 1 deletion extensions/markdown-language-features/tsconfig.json
Expand Up @@ -6,6 +6,7 @@
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts",
"../../src/vscode-dts/vscode.proposed.documentPaste.d.ts"
"../../src/vscode-dts/vscode.proposed.documentPaste.d.ts",
"../../src/vscode-dts/vscode.proposed.dropMetadata.d.ts"
]
}
3 changes: 2 additions & 1 deletion src/vs/editor/browser/widget/codeEditorWidget.ts
Expand Up @@ -2242,7 +2242,7 @@ class EditorDecorationsCollection implements editorCommon.IEditorDecorationsColl
this.set([]);
}

public set(newDecorations: IModelDeltaDecoration[]): void {
public set(newDecorations: readonly IModelDeltaDecoration[]): string[] {
try {
this._isChangingDecorations = true;
this._editor.changeDecorations((accessor) => {
Expand All @@ -2251,6 +2251,7 @@ class EditorDecorationsCollection implements editorCommon.IEditorDecorationsColl
} finally {
this._isChangingDecorations = false;
}
return this._decorationIds;
}
}

Expand Down
26 changes: 23 additions & 3 deletions src/vs/editor/common/config/editorOptions.ts
Expand Up @@ -4701,10 +4701,16 @@ class EditorWrappingInfoComputer extends ComputedEditorOption<EditorOption.wrapp
*/
export interface IDropIntoEditorOptions {
/**
* Enable the dropping into editor.
* Enable dropping into editor.
* Defaults to true.
*/
enabled?: boolean;

/**
* Controls if a widget is shown after a drop.
* Defaults to 'afterDrop'.
*/
showDropSelector?: 'afterDrop' | 'never';
}

/**
Expand All @@ -4715,7 +4721,7 @@ export type EditorDropIntoEditorOptions = Readonly<Required<IDropIntoEditorOptio
class EditorDropIntoEditor extends BaseEditorOption<EditorOption.dropIntoEditor, IDropIntoEditorOptions, EditorDropIntoEditorOptions> {

constructor() {
const defaults: EditorDropIntoEditorOptions = { enabled: true };
const defaults: EditorDropIntoEditorOptions = { enabled: true, showDropSelector: 'afterDrop' };
super(
EditorOption.dropIntoEditor, 'dropIntoEditor', defaults,
{
Expand All @@ -4724,6 +4730,19 @@ class EditorDropIntoEditor extends BaseEditorOption<EditorOption.dropIntoEditor,
default: defaults.enabled,
markdownDescription: nls.localize('dropIntoEditor.enabled', "Controls whether you can drag and drop a file into a text editor by holding down `shift` (instead of opening the file in an editor)."),
},
'editor.dropIntoEditor.showDropSelector': {
type: 'string',
markdownDescription: nls.localize('dropIntoEditor.showDropSelector', "Controls if a widget is shown when dropping files into the editor. This widget lets you control how the file is dropped."),
enum: [
'afterDrop',
'never'
],
enumDescriptions: [
nls.localize('dropIntoEditor.showDropSelector.afterDrop', "Show the drop selector widget after a file is dropped into the editor."),
nls.localize('dropIntoEditor.showDropSelector.never', "Never show the drop selector widget. Instead the default drop provider is always used."),
],
default: 'afterDrop',
},
}
);
}
Expand All @@ -4734,7 +4753,8 @@ class EditorDropIntoEditor extends BaseEditorOption<EditorOption.dropIntoEditor,
}
const input = _input as IDropIntoEditorOptions;
return {
enabled: boolean(input.enabled, this.defaultValue.enabled)
enabled: boolean(input.enabled, this.defaultValue.enabled),
showDropSelector: stringSet(input.showDropSelector, this.defaultValue.showDropSelector, ['afterDrop', 'never']),
};
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/vs/editor/common/editorCommon.ts
Expand Up @@ -545,7 +545,7 @@ export interface IEditorDecorationsCollection {
/**
* Replace all previous decorations with `newDecorations`.
*/
set(newDecorations: IModelDeltaDecoration[]): void;
set(newDecorations: readonly IModelDeltaDecoration[]): string[];
/**
* Remove all previous decorations.
*/
Expand Down
5 changes: 5 additions & 0 deletions src/vs/editor/common/languages.ts
Expand Up @@ -1917,6 +1917,8 @@ export enum ExternalUriOpenerPriority {
* @internal
*/
export interface DocumentOnDropEdit {
readonly label: string;

insertText: string | { snippet: string };
additionalEdit?: WorkspaceEdit;
}
Expand All @@ -1925,5 +1927,8 @@ export interface DocumentOnDropEdit {
* @internal
*/
export interface DocumentOnDropEditProvider {
readonly id?: string;
readonly dropMimeTypes?: readonly string[];

provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken): ProviderResult<DocumentOnDropEdit>;
}
2 changes: 1 addition & 1 deletion src/vs/editor/common/model.ts
Expand Up @@ -307,7 +307,7 @@ export interface IModelDecorationsChangeAccessor {
* @param newDecorations Array describing what decorations should result after the call.
* @return An array containing the new decorations identifiers.
*/
deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[];
deltaDecorations(oldDecorations: readonly string[], newDecorations: readonly IModelDeltaDecoration[]): string[];
}

/**
Expand Down
109 changes: 109 additions & 0 deletions src/vs/editor/contrib/dropIntoEditor/browser/defaultOnDropProviders.ts
@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationToken } from 'vs/base/common/cancellation';
import { UriList, VSDataTransfer } from 'vs/base/common/dataTransfer';
import { Mimes } from 'vs/base/common/mime';
import { Schemas } from 'vs/base/common/network';
import { relativePath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { IPosition } from 'vs/editor/common/core/position';
import { DocumentOnDropEdit, DocumentOnDropEditProvider } from 'vs/editor/common/languages';
import { ITextModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { localize } from 'vs/nls';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';

class DefaultTextDropProvider implements DocumentOnDropEditProvider {

readonly id = 'text';
readonly dropMimeTypes = [Mimes.text, 'text'];

async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: VSDataTransfer, _token: CancellationToken): Promise<DocumentOnDropEdit | undefined> {
const textEntry = dataTransfer.get('text') ?? dataTransfer.get(Mimes.text);
if (textEntry) {
const text = await textEntry.asString();
return {
label: localize('defaultDropProvider.text.label', "Drop as plain text"),
insertText: text
};
}

return undefined;
}
}

class DefaultUriListDropProvider implements DocumentOnDropEditProvider {

readonly id = 'uri';
readonly dropMimeTypes = [Mimes.uriList];

constructor(
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService
) { }

async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: VSDataTransfer, _token: CancellationToken): Promise<DocumentOnDropEdit | undefined> {
const urlListEntry = dataTransfer.get(Mimes.uriList);
if (urlListEntry) {
const urlList = await urlListEntry.asString();
const entry = this.getUriListInsertText(urlList);
if (entry) {
return {
label: entry.count > 1
? localize('defaultDropProvider.uri.label', "Drop as uri")
: localize('defaultDropProvider.uriList.label', "Drop as uri list"),
insertText: entry.snippet
};
}
}

return undefined;
}

private getUriListInsertText(strUriList: string): { snippet: string; count: number } | undefined {
const entries: { readonly uri: URI; readonly originalText: string }[] = [];
for (const entry of UriList.parse(strUriList)) {
try {
entries.push({ uri: URI.parse(entry), originalText: entry });
} catch {
// noop
}
}

if (!entries.length) {
return;
}

const snippet = entries
.map(({ uri, originalText }) => {
const root = this._workspaceContextService.getWorkspaceFolder(uri);
if (root) {
const rel = relativePath(root.uri, uri);
if (rel) {
return rel;
}
}

return uri.scheme === Schemas.file ? uri.fsPath : originalText;
})
.join(' ');

return { snippet, count: entries.length };
}
}


let registeredDefaultProviders = false;
export function registerDefaultDropProviders(
languageFeaturesService: ILanguageFeaturesService,
workspaceContextService: IWorkspaceContextService,
) {
if (!registeredDefaultProviders) {
registeredDefaultProviders = true;

languageFeaturesService.documentOnDropEditProvider.register('*', new DefaultTextDropProvider());
languageFeaturesService.documentOnDropEditProvider.register('*', new DefaultUriListDropProvider(workspaceContextService));
}
}