Align custom editor API proposal with notebook API

Fixes #95854
Fixes #95849
For #77131

- Move all editing functionality back onto the provider. This better matches the notebook API.

- Rename `CustomEditorProvider` to `CustomReadonlyEditorProvider`.  `CustomEditorProvider` is now how editable custom editors are implemented

- Give extension a full suggested backup path instead of just a folder
pull/96108/head
Matt Bierner 2020-04-24 14:47:00 -07:00
parent 37944c88ac
commit 4862602c4c
4 changed files with 188 additions and 157 deletions

View File

@ -13,7 +13,7 @@ import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry';
const localize = nls.loadMessageBundle();
export class PreviewManager implements vscode.CustomEditorProvider {
export class PreviewManager implements vscode.CustomReadonlyEditorProvider {
public static readonly viewType = 'imagePreview.previewEditor';

View File

@ -1239,11 +1239,17 @@ declare module 'vscode' {
}
/**
* Event triggered by extensions to signal to VS Code that an edit has occurred on an [`CustomEditableDocument`](#CustomEditableDocument).
* Event triggered by extensions to signal to VS Code that an edit has occurred on an [`CustomDocument`](#CustomDocument).
*
* @see [`CustomEditableDocument.onDidChange`](#CustomEditableDocument.onDidChange).
* @see [`CustomDocumentProvider.onDidChangeCustomDocument`](#CustomDocumentProvider.onDidChangeCustomDocument).
*/
interface CustomDocumentEditEvent {
/**
* The document that the edit is for.
*/
readonly document: CustomDocument;
/**
* Undo the edit operation.
*
@ -1267,17 +1273,20 @@ declare module 'vscode' {
}
/**
* Event triggered by extensions to signal to VS Code that the content of a [`CustomEditableDocument`](#CustomEditableDocument)
* Event triggered by extensions to signal to VS Code that the content of a [`CustomDocument`](#CustomDocument)
* has changed.
*
* @see [`CustomEditableDocument.onDidChange`](#CustomEditableDocument.onDidChange).
* @see [`CustomDocumentProvider.onDidChangeCustomDocument`](#CustomDocumentProvider.onDidChangeCustomDocument).
*/
interface CustomDocumentContentChangeEvent {
// marker interface
/**
* The document that the change is for.
*/
readonly document: CustomDocument;
}
/**
* A backup for an [`CustomEditableDocument`](#CustomEditableDocument).
* A backup for an [`CustomDocument`](#CustomDocument).
*/
interface CustomDocumentBackup {
/**
@ -1285,7 +1294,7 @@ declare module 'vscode' {
*
* This id is passed back to your extension in `openCustomDocument` when opening a custom editor from a backup.
*/
readonly backupId: string;
readonly id: string;
/**
* Delete the current backup.
@ -1301,113 +1310,13 @@ declare module 'vscode' {
*/
interface CustomDocumentBackupContext {
/**
* Uri of a workspace specific directory in which the extension can store backup data.
* Suggested file location to write the new backup.
*
* The directory might not exist on disk and creation is up to the extension.
* Note that your extension is free to ignore this and use its own strategy for backup.
*
* For editors for workspace resource, this destination will be in the workspace storage. The path may not
*/
readonly workspaceStorageUri: Uri | undefined;
}
/**
* Represents an editable custom document used by a [`CustomEditorProvider`](#CustomEditorProvider).
*
* `CustomEditableDocument` is how custom editors hook into standard VS Code operations such as save and undo. The
* document is also how custom editors notify VS Code that an edit has taken place.
*/
interface CustomEditableDocument extends CustomDocument {
/**
* Signal that an edit has occurred inside a custom editor.
*
* This event must be fired by your extension whenever an edit happens in a custom editor. An edit can be
* anything from changing some text, to cropping an image, to reordering a list. Your extension is free to
* define what an edit is and what data is stored on each edit.
*
* Firing `onDidChange` causes VS Code to mark the editors as being dirty. This is cleared when the user either
* saves or reverts the file.
*
* Editors that support undo/redo must fire a `CustomDocumentEditEvent` whenever an edit happens. This allows
* users to undo and redo the edit using VS Code's standard VS Code keyboard shortcuts. VS Code will also mark
* the editor as no longer being dirty if the user undoes all edits to the last saved state.
*
* Editors that support editing but cannot use VS Code's standard undo/redo mechanism must fire a `CustomDocumentContentChangeEvent`.
* The only way for a user to clear the dirty state of an editor that does not support undo/redo is to either
* `save` or `revert` the file.
*
* An editor should only ever fire `CustomDocumentEditEvent` events, or only ever fire `CustomDocumentContentChangeEvent` events.
*/
readonly onDidChange: Event<CustomDocumentEditEvent> | Event<CustomDocumentContentChangeEvent>;
/**
* Save the resource for a custom editor.
*
* This method is invoked by VS Code when the user saves a custom editor. This can happen when the user
* triggers save while the custom editor is active, by commands such as `save all`, or by auto save if enabled.
*
* To implement `save`, the implementer must persist the custom editor. This usually means writing the
* file data for the custom document to disk. After `save` completes, any associated editor instances will
* no longer be marked as dirty.
*
* @param cancellation Token that signals the save is no longer required (for example, if another save was triggered).
*
* @return Thenable signaling that saving has completed.
*/
save(cancellation: CancellationToken): Thenable<void>;
/**
* Save the resource for a custom editor to a different location.
*
* This method is invoked by VS Code when the user triggers save as on a custom editor. The implementer must
* persist the custom editor to `targetResource`.
*
* When the user accepts save as, the current editor is be replaced by an editor for the newly saved file.
*
* @param uri Location to save to.
* @param cancellation Token that signals the save is no longer required.
*
* @return Thenable signaling that saving has completed.
*/
saveAs(uri: Uri, cancellation: CancellationToken): Thenable<void>;
/**
* Revert a custom editor to its last saved state.
*
* This method is invoked by VS Code when the user triggers `File: Revert File` in a custom editor. (Note that
* this is only used using VS Code's `File: Revert File` command and not on a `git revert` of the file).
*
* To implement `revert`, the implementer must make sure all editor instances (webviews) for `document`
* are displaying the document in the same state is saved in. This usually means reloading the file from the
* workspace.
*
* During `revert`, your extension should also clear any backups for the custom editor. Backups are only needed
* when there is a difference between an editor's state in VS Code and its save state on disk.
*
* @param cancellation Token that signals the revert is no longer required.
*
* @return Thenable signaling that the change has completed.
*/
revert(cancellation: CancellationToken): Thenable<void>;
/**
* Back up the resource in its current state.
*
* Backups are used for hot exit and to prevent data loss. Your `backup` method should persist the resource in
* its current state, i.e. with the edits applied. Most commonly this means saving the resource to disk in
* the `ExtensionContext.storagePath`. When VS Code reloads and your custom editor is opened for a resource,
* your extension should first check to see if any backups exist for the resource. If there is a backup, your
* extension should load the file contents from there instead of from the resource in the workspace.
*
* `backup` is triggered whenever an edit it made. Calls to `backup` are debounced so that if multiple edits are
* made in quick succession, `backup` is only triggered after the last one. `backup` is not invoked when
* `auto save` is enabled (since auto save already persists resource ).
*
* @param context Information that can be used to backup the document.
* @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your
* extension to decided how to respond to cancellation. If for example your extension is backing up a large file
* in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather
* than cancelling it to ensure that VS Code has some valid backup.
*/
backup(context: CustomDocumentBackupContext, cancellation: CancellationToken): Thenable<CustomDocumentBackup>;
readonly destination: Uri;
}
/**
@ -1434,7 +1343,7 @@ declare module 'vscode' {
*
* @param T Type of the custom document returned by this provider.
*/
export interface CustomEditorProvider<T extends CustomDocument = CustomDocument> {
export interface CustomReadonlyEditorProvider<T extends CustomDocument = CustomDocument> {
/**
* Create a new document for a given resource.
@ -1470,17 +1379,118 @@ declare module 'vscode' {
resolveCustomEditor(document: T, webviewPanel: WebviewPanel, token: CancellationToken): Thenable<void> | void;
}
export interface CustomEditorProvider<T extends CustomDocument = CustomDocument> extends CustomReadonlyEditorProvider<T> {
/**
* Signal that an edit has occurred inside a custom editor.
*
* This event must be fired by your extension whenever an edit happens in a custom editor. An edit can be
* anything from changing some text, to cropping an image, to reordering a list. Your extension is free to
* define what an edit is and what data is stored on each edit.
*
* Firing `onDidChange` causes VS Code to mark the editors as being dirty. This is cleared when the user either
* saves or reverts the file.
*
* Editors that support undo/redo must fire a `CustomDocumentEditEvent` whenever an edit happens. This allows
* users to undo and redo the edit using VS Code's standard VS Code keyboard shortcuts. VS Code will also mark
* the editor as no longer being dirty if the user undoes all edits to the last saved state.
*
* Editors that support editing but cannot use VS Code's standard undo/redo mechanism must fire a `CustomDocumentContentChangeEvent`.
* The only way for a user to clear the dirty state of an editor that does not support undo/redo is to either
* `save` or `revert` the file.
*
* An editor should only ever fire `CustomDocumentEditEvent` events, or only ever fire `CustomDocumentContentChangeEvent` events.
*/
readonly onDidChangeCustomDocument: Event<CustomDocumentEditEvent> | Event<CustomDocumentContentChangeEvent>;
/**
* Save a custom document.
*
* This method is invoked by VS Code when the user saves a custom editor. This can happen when the user
* triggers save while the custom editor is active, by commands such as `save all`, or by auto save if enabled.
*
* To implement `save`, the implementer must persist the custom editor. This usually means writing the
* file data for the custom document to disk. After `save` completes, any associated editor instances will
* no longer be marked as dirty.
*
* @param document Document to save.
* @param cancellation Token that signals the save is no longer required (for example, if another save was triggered).
*
* @return Thenable signaling that saving has completed.
*/
saveCustomDocument(document: CustomDocument, cancellation: CancellationToken): Thenable<void>;
/**
* Save a custom document to a different location.
*
* This method is invoked by VS Code when the user triggers 'save as' on a custom editor. The implementer must
* persist the custom editor to `destination`.
*
* When the user accepts save as, the current editor is be replaced by an editor for the newly saved file.
*
* @param document Document to save.
* @param destination Location to save to.
* @param cancellation Token that signals the save is no longer required.
*
* @return Thenable signaling that saving has completed.
*/
saveCustomDocumentAs(document: CustomDocument, destination: Uri, cancellation: CancellationToken): Thenable<void>;
/**
* Revert a custom document to its last saved state.
*
* This method is invoked by VS Code when the user triggers `File: Revert File` in a custom editor. (Note that
* this is only used using VS Code's `File: Revert File` command and not on a `git revert` of the file).
*
* To implement `revert`, the implementer must make sure all editor instances (webviews) for `document`
* are displaying the document in the same state is saved in. This usually means reloading the file from the
* workspace.
*
* During `revert`, your extension should also clear any backups for the custom editor. Backups are only needed
* when there is a difference between an editor's state in VS Code and its save state on disk.
*
* @param document Document to revert.
* @param cancellation Token that signals the revert is no longer required.
*
* @return Thenable signaling that the change has completed.
*/
revertCustomDocument(document: CustomDocument, cancellation: CancellationToken): Thenable<void>;
/**
* Back up a dirty custom document.
*
* Backups are used for hot exit and to prevent data loss. Your `backup` method should persist the resource in
* its current state, i.e. with the edits applied. Most commonly this means saving the resource to disk in
* the `ExtensionContext.storagePath`. When VS Code reloads and your custom editor is opened for a resource,
* your extension should first check to see if any backups exist for the resource. If there is a backup, your
* extension should load the file contents from there instead of from the resource in the workspace.
*
* `backup` is triggered whenever an edit it made. Calls to `backup` are debounced so that if multiple edits are
* made in quick succession, `backup` is only triggered after the last one. `backup` is not invoked when
* `auto save` is enabled (since auto save already persists resource ).
*
* @param document Document to backup.
* @param context Information that can be used to backup the document.
* @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your
* extension to decided how to respond to cancellation. If for example your extension is backing up a large file
* in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather
* than cancelling it to ensure that VS Code has some valid backup.
*/
backupCustomDocument(document: CustomDocument, context: CustomDocumentBackupContext, cancellation: CancellationToken): Thenable<CustomDocumentBackup>;
}
namespace window {
/**
* Temporary overload for `registerCustomEditorProvider` that takes a `CustomEditorProvider`.
*/
export function registerCustomEditorProvider2(
viewType: string,
provider: CustomEditorProvider,
provider: CustomReadonlyEditorProvider | CustomEditorProvider,
options?: {
readonly webviewOptions?: WebviewPanelOptions;
/**
* Only applies to `CustomReadonlyEditorProvider | CustomEditorProvider`.
*
* Indicates that the provider allows multiple editor instances to be open at the same time for
* the same resource.
*

View File

@ -588,7 +588,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
registerCustomEditorProvider: (viewType: string, provider: vscode.CustomTextEditorProvider, options: { webviewOptions?: vscode.WebviewPanelOptions } = {}) => {
return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options);
},
registerCustomEditorProvider2: (viewType: string, provider: vscode.CustomEditorProvider, options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean } = {}) => {
registerCustomEditorProvider2: (viewType: string, provider: vscode.CustomReadonlyEditorProvider, options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean } = {}) => {
checkProposedApiEnabled(extension);
return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options);
},

View File

@ -5,7 +5,10 @@
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { hash } from 'vs/base/common/hash';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { joinPath } from 'vs/base/common/resources';
import { URI, UriComponents } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import * as modes from 'vs/editor/common/modes';
@ -13,6 +16,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'
import { ILogService } from 'vs/platform/log/common/log';
import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor';
@ -21,7 +25,6 @@ import type * as vscode from 'vscode';
import { Cache } from './cache';
import * as extHostProtocol from './extHost.protocol';
import * as extHostTypes from './extHostTypes';
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
type IconPath = URI | { light: URI, dark: URI };
@ -266,16 +269,17 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa
class CustomDocumentStoreEntry {
private _backupCounter = 1;
constructor(
public readonly document: vscode.CustomDocument,
public readonly backupContext: vscode.CustomDocumentBackupContext,
private readonly _storagePath: string,
) { }
private readonly _edits = new Cache<vscode.CustomDocumentEditEvent>('custom documents');
private _backup?: vscode.CustomDocumentBackup;
addEdit(item: vscode.CustomDocumentEditEvent): number {
return this._edits.add([item]);
}
@ -300,6 +304,10 @@ class CustomDocumentStoreEntry {
}
}
getNewBackupUri(): URI {
return joinPath(URI.file(this._storagePath), hashPath(this.document.uri) + (this._backupCounter++));
}
updateBackup(backup: vscode.CustomDocumentBackup): void {
this._backup?.delete();
this._backup = backup;
@ -326,12 +334,12 @@ class CustomDocumentStore {
return this._documents.get(this.key(viewType, resource));
}
public add(viewType: string, document: vscode.CustomDocument, backupContext: vscode.CustomDocumentBackupContext): CustomDocumentStoreEntry {
public add(viewType: string, document: vscode.CustomDocument, storagePath: string): CustomDocumentStoreEntry {
const key = this.key(viewType, document.uri);
if (this._documents.has(key)) {
throw new Error(`Document already exists for viewType:${viewType} resource:${document.uri}`);
}
const entry = new CustomDocumentStoreEntry(document, backupContext);
const entry = new CustomDocumentStoreEntry(document, storagePath);
this._documents.set(key, entry);
return entry;
}
@ -359,7 +367,7 @@ type ProviderEntry = {
} | {
readonly extension: IExtensionDescription;
readonly type: WebviewEditorType.Custom;
readonly provider: vscode.CustomEditorProvider;
readonly provider: vscode.CustomReadonlyEditorProvider;
};
class EditorProviderStore {
@ -369,7 +377,7 @@ class EditorProviderStore {
return this.add(WebviewEditorType.Text, viewType, extension, provider);
}
public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomEditorProvider): vscode.Disposable {
public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomReadonlyEditorProvider): vscode.Disposable {
return this.add(WebviewEditorType.Custom, viewType, extension, provider);
}
@ -377,7 +385,7 @@ class EditorProviderStore {
return this._providers.get(viewType);
}
private add(type: WebviewEditorType, viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider): vscode.Disposable {
private add(type: WebviewEditorType, viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider): vscode.Disposable {
if (this._providers.has(viewType)) {
throw new Error(`Provider for viewType:${viewType} already registered`);
}
@ -459,7 +467,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
public registerCustomEditorProvider(
extension: IExtensionDescription,
viewType: string,
provider: vscode.CustomEditorProvider | vscode.CustomTextEditorProvider,
provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider,
options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean },
): vscode.Disposable {
const disposables = new DisposableStore();
@ -470,6 +478,19 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
});
} else {
disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider));
if (this.supportEditing(provider)) {
disposables.add(provider.onDidChangeCustomDocument(e => {
const entry = this.getCustomDocumentEntry(viewType, e.document.uri);
if (isEditEvent(e)) {
const editId = entry.addEdit(e);
this._proxy.$onDidEdit(e.document.uri, viewType, editId, e.label);
} else {
this._proxy.$onContentChange(e.document.uri, viewType);
}
}));
}
this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, !!options.supportsMultipleEditorsPerDocument);
}
@ -570,23 +591,11 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
const revivedResource = URI.revive(resource);
const document = await entry.provider.openCustomDocument(revivedResource, { backupId }, cancellation);
const workspaceStoragePath = this._extensionStoragePaths?.workspaceValue(entry.extension);
const documentEntry = this._documents.add(viewType, document, {
workspaceStorageUri: workspaceStoragePath ? URI.file(workspaceStoragePath) : undefined,
});
if (this.isEditable(document)) {
document.onDidChange(e => {
if (isEditEvent(e)) {
const editId = documentEntry.addEdit(e);
this._proxy.$onDidEdit(document.uri, viewType, editId, e.label);
} else {
this._proxy.$onContentChange(document.uri, viewType);
}
});
}
const storageRoot = this._extensionStoragePaths?.workspaceValue(entry.extension) ?? this._extensionStoragePaths?.globalValue(entry.extension);
this._documents.add(viewType, document, storageRoot!);
return { editable: this.isEditable(document) };
return { editable: this.supportEditing(entry.provider) };
}
async $disposeCustomDocument(resource: UriComponents, viewType: string): Promise<void> {
@ -680,29 +689,33 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
async $revert(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
const document = this.getCustomEditableDocument(viewType, resourceComponents);
await document.revert(cancellation);
const provider = this.getCustomEditorProvider(viewType);
await provider.revertCustomDocument(entry.document, cancellation);
entry.disposeBackup();
}
async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void> {
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
const document = this.getCustomEditableDocument(viewType, resourceComponents);
await document.save(cancellation);
const provider = this.getCustomEditorProvider(viewType);
await provider.saveCustomDocument(entry.document, cancellation);
entry.disposeBackup();
}
async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise<void> {
const document = this.getCustomEditableDocument(viewType, resourceComponents);
return document.saveAs(URI.revive(targetResource), cancellation);
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
const provider = this.getCustomEditorProvider(viewType);
return provider.saveCustomDocumentAs(entry.document, URI.revive(targetResource), cancellation);
}
async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise<string> {
const entry = this.getCustomDocumentEntry(viewType, resourceComponents);
const document = this.getCustomEditableDocument(viewType, resourceComponents);
const backup = await document.backup(entry.backupContext, cancellation);
const provider = this.getCustomEditorProvider(viewType);
const backup = await provider.backupCustomDocument(entry.document, {
destination: entry.getNewBackupUri(),
}, cancellation);
entry.updateBackup(backup);
return backup.backupId;
return backup.id;
}
private getWebviewPanel(handle: extHostProtocol.WebviewPanelHandle): ExtHostWebviewEditor | undefined {
@ -717,16 +730,19 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape {
return entry;
}
private isEditable(document: vscode.CustomDocument): document is vscode.CustomEditableDocument {
return !!(document as vscode.CustomEditableDocument).onDidChange;
}
private getCustomEditableDocument(viewType: string, resource: UriComponents): vscode.CustomEditableDocument {
const { document } = this.getCustomDocumentEntry(viewType, resource);
if (!this.isEditable(document)) {
private getCustomEditorProvider(viewType: string): vscode.CustomEditorProvider {
const entry = this._editorProviders.get(viewType);
const provider = entry?.provider;
if (!provider || !this.supportEditing(provider)) {
throw new Error('Custom document is not editable');
}
return document;
return provider;
}
private supportEditing(
provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider | vscode.CustomReadonlyEditorProvider
): provider is vscode.CustomEditorProvider {
return !!(provider as vscode.CustomEditorProvider).onDidChangeCustomDocument;
}
}
@ -759,3 +775,8 @@ function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomD
return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function'
&& typeof (e as vscode.CustomDocumentEditEvent).redo === 'function';
}
function hashPath(resource: URI): string {
const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString();
return hash(str) + '';
}