diff --git a/extensions/git/package.json b/extensions/git/package.json index 995ac785f74..a207c29d6a5 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -693,7 +693,7 @@ { "command": "git.openChange", "group": "navigation", - "when": "config.git.enabled && gitOpenRepositoryCount != 0 && !isInDiffEditor && resourceScheme != extension" + "when": "config.git.enabled && gitOpenRepositoryCount != 0 && !isInDiffEditor && resourceScheme == file" }, { "command": "git.stageSelectedRanges", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index d49e59d2eeb..7b4c807989d 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -174,7 +174,7 @@ export class CommandCenter { const activeTextEditor = window.activeTextEditor; - if (preserveSelection && activeTextEditor && activeTextEditor.document.uri.fsPath === right.fsPath) { + if (preserveSelection && activeTextEditor && activeTextEditor.document.uri.toString() === right.toString()) { opts.selection = activeTextEditor.selection; } @@ -368,7 +368,7 @@ export class CommandCenter { viewColumn: ViewColumn.Active }; - if (activeTextEditor && activeTextEditor.document.uri.fsPath === uri.fsPath) { + if (activeTextEditor && activeTextEditor.document.uri.toString() === uri.toString()) { opts.selection = activeTextEditor.selection; } diff --git a/extensions/git/src/contentProvider.ts b/extensions/git/src/contentProvider.ts index f47765c484e..ff8529d1612 100644 --- a/extensions/git/src/contentProvider.ts +++ b/extensions/git/src/contentProvider.ts @@ -101,7 +101,7 @@ export class GitContentProvider { Object.keys(this.cache).forEach(key => { const row = this.cache[key]; - const isOpen = window.visibleTextEditors.some(e => e.document.uri.fsPath === row.uri.fsPath); + const isOpen = window.visibleTextEditors.some(e => e.document.toString() === row.uri.toString()); if (isOpen || now - row.timestamp < THREE_MINUTES) { cache[row.uri.toString()] = row; diff --git a/extensions/markdown/src/extension.ts b/extensions/markdown/src/extension.ts index 2dd53b296d7..2d6bf4d1877 100644 --- a/extensions/markdown/src/extension.ts +++ b/extensions/markdown/src/extension.ts @@ -117,7 +117,7 @@ export function activate(context: vscode.ExtensionContext) { logger.log('revealLine', { uri, sourceUri: sourceUri.toString(), line }); vscode.window.visibleTextEditors - .filter(editor => isMarkdownFile(editor.document) && editor.document.uri.fsPath === sourceUri.fsPath) + .filter(editor => isMarkdownFile(editor.document) && editor.document.uri.toString() === sourceUri.toString()) .forEach(editor => { const sourceLine = Math.floor(line); const fraction = line - sourceLine; @@ -296,7 +296,7 @@ function showSource(mdUri: vscode.Uri) { const docUri = vscode.Uri.parse(mdUri.query); for (const editor of vscode.window.visibleTextEditors) { - if (editor.document.uri.scheme === docUri.scheme && editor.document.uri.fsPath === docUri.fsPath) { + if (editor.document.uri.scheme === docUri.scheme && editor.document.uri.toString() === docUri.toString()) { return vscode.window.showTextDocument(editor.document, editor.viewColumn); } } diff --git a/extensions/markdown/src/previewContentProvider.ts b/extensions/markdown/src/previewContentProvider.ts index a7eef2330a3..870816c1eeb 100644 --- a/extensions/markdown/src/previewContentProvider.ts +++ b/extensions/markdown/src/previewContentProvider.ts @@ -205,7 +205,7 @@ export class MDDocumentContentProvider implements vscode.TextDocumentContentProv let initialLine: number | undefined = undefined; const editor = vscode.window.activeTextEditor; - if (editor && editor.document.uri.fsPath === sourceUri.fsPath) { + if (editor && editor.document.uri.toString() === sourceUri.toString()) { initialLine = editor.selection.active.line; } diff --git a/extensions/typescript/src/features/codeActionProvider.ts b/extensions/typescript/src/features/codeActionProvider.ts index fefe85a3835..037a1039dc7 100644 --- a/extensions/typescript/src/features/codeActionProvider.ts +++ b/extensions/typescript/src/features/codeActionProvider.ts @@ -121,7 +121,7 @@ export default class TypeScriptCodeActionProvider implements CodeActionProvider let firstEdit: TextEdit | undefined = undefined; for (const [uri, edits] of workspaceEdit.entries()) { - if (uri.fsPath === source.uri.fsPath) { + if (uri.toString() === source.uri.toString()) { firstEdit = edits[0]; break; } diff --git a/extensions/typescript/src/features/implementationsCodeLensProvider.ts b/extensions/typescript/src/features/implementationsCodeLensProvider.ts index a36ab6b2527..b51da2970a2 100644 --- a/extensions/typescript/src/features/implementationsCodeLensProvider.ts +++ b/extensions/typescript/src/features/implementationsCodeLensProvider.ts @@ -53,7 +53,7 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip reference.start.line, 0))) // Exclude original from implementations .filter(location => - !(location.uri.fsPath === codeLens.document.fsPath && + !(location.uri.toString() === codeLens.document.toString() && location.range.start.line === codeLens.range.start.line && location.range.start.character === codeLens.range.start.character)); diff --git a/extensions/typescript/src/features/referencesCodeLensProvider.ts b/extensions/typescript/src/features/referencesCodeLensProvider.ts index 251337ab59c..641dc28cf8a 100644 --- a/extensions/typescript/src/features/referencesCodeLensProvider.ts +++ b/extensions/typescript/src/features/referencesCodeLensProvider.ts @@ -47,7 +47,7 @@ export default class TypeScriptReferencesCodeLensProvider extends TypeScriptBase new Location(this.client.asUrl(reference.file), tsTextSpanToVsRange(reference))) .filter(location => // Exclude original definition from references - !(location.uri.fsPath === codeLens.document.fsPath && + !(location.uri.toString() === codeLens.document.toString() && location.range.start.isEqual(codeLens.range.start))); codeLens.command = { diff --git a/package.json b/package.json index bea37d0dda1..dba3a582d9e 100644 --- a/package.json +++ b/package.json @@ -132,4 +132,4 @@ "windows-mutex": "^0.2.0", "fsevents": "0.3.8" } -} \ No newline at end of file +} diff --git a/src/vs/base/common/labels.ts b/src/vs/base/common/labels.ts index 86b6fad0efa..443ca60c1f0 100644 --- a/src/vs/base/common/labels.ts +++ b/src/vs/base/common/labels.ts @@ -36,6 +36,9 @@ export function getPathLabel(resource: URI | string, rootProvider?: IWorkspaceFo if (typeof resource === 'string') { resource = URI.file(resource); } + if (resource.scheme !== 'file' && resource.scheme !== 'untitled') { + return resource.authority + resource.path; + } // return early if we can resolve a relative path label from the root const baseResource = rootProvider ? rootProvider.getWorkspaceFolder(resource) : null; diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts new file mode 100644 index 00000000000..50ebdc2069f --- /dev/null +++ b/src/vs/base/common/resources.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as paths from 'vs/base/common/paths'; +import uri from 'vs/base/common/uri'; + +export function basenameOrAuthority(resource: uri): string { + return paths.basename(resource.fsPath) || resource.authority; +} + +export function isEqualOrParent(first: uri, second: uri, ignoreCase?: boolean): boolean { + if (first.scheme === second.scheme && first.authority === second.authority) { + return paths.isEqualOrParent(first.fsPath, second.fsPath, ignoreCase); + } + + return false; +} + +export function dirname(resource: uri): uri { + return resource.with({ + path: paths.dirname(resource.path) + }); +} diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index c89cc6399bf..469fe9552c6 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -13,6 +13,8 @@ import { isLinux } from 'vs/base/common/platform'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import Event from 'vs/base/common/event'; import { beginsWithIgnoreCase } from 'vs/base/common/strings'; +import { IProgress } from 'vs/platform/progress/common/progress'; +import { IDisposable } from 'vs/base/common/lifecycle'; export const IFileService = createDecorator('fileService'); @@ -30,6 +32,13 @@ export interface IFileService { */ onAfterOperation: Event; + /** + * + */ + registerProvider?(authority: string, provider: IFileSystemProvider): IDisposable; + + supportResource?(resource: URI): boolean; + /** * Resolve the properties of a file identified by the resource. * @@ -150,6 +159,37 @@ export interface IFileService { dispose(): void; } + +export enum FileType { + File = 0, + Dir = 1, + Symlink = 2 +} +export interface IStat { + resource: URI; + mtime: number; + size: number; + type: FileType; +} + +export interface IFileSystemProvider { + + onDidChange?: Event; + + // more... + // + utimes(resource: URI, mtime: number): TPromise; + stat(resource: URI): TPromise; + read(resource: URI, progress: IProgress): TPromise; + write(resource: URI, content: Uint8Array): TPromise; + unlink(resource: URI): TPromise; + rename(resource: URI, target: URI): TPromise; + mkdir(resource: URI): TPromise; + readdir(resource: URI): TPromise; + rmdir(resource: URI): TPromise; +} + + export enum FileOperation { CREATE, DELETE, diff --git a/src/vs/platform/opener/browser/openerService.ts b/src/vs/platform/opener/browser/openerService.ts index f6d6e7a0287..d567a6f5779 100644 --- a/src/vs/platform/opener/browser/openerService.ts +++ b/src/vs/platform/opener/browser/openerService.ts @@ -73,7 +73,7 @@ export class OpenerService implements IOpenerService { return TPromise.as(undefined); } else if (resource.scheme === Schemas.file) { - resource = URI.file(normalize(resource.fsPath)); // workaround for non-normalized paths (https://github.com/Microsoft/vscode/issues/12954) + resource = resource.with({ path: normalize(resource.path) }); // workaround for non-normalized paths (https://github.com/Microsoft/vscode/issues/12954) } promise = this._editorService.openEditor({ resource, options: { selection, } }, options && options.openToSide); } diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index e923eb66709..b03e37e9e5f 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -190,13 +190,13 @@ export class Workspace implements IWorkspace { return null; } - return this._foldersMap.findSubstr(resource.fsPath); + return this._foldersMap.findSubstr(resource.toString()); } private updateFoldersMap(): void { this._foldersMap = new TrieMap(); for (const folder of this.folders) { - this._foldersMap.insert(folder.uri.fsPath, folder); + this._foldersMap.insert(folder.uri.toString(), folder); } } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index cadf55ea65c..42f021d9f85 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -50,17 +50,48 @@ declare module 'vscode' { ignoreFocusOut?: boolean; } + export enum FileChangeType { + Updated = 0, + Added = 1, + Deleted = 2 + } + + export interface FileChange { + type: FileChangeType; + resource: Uri; + } + + export enum FileType { + File = 0, + Dir = 1, + Symlink = 2 + } + + export interface FileStat { + resource: Uri; + mtime: number; + size: number; + type: FileType; + } + // todo@joh discover files etc export interface FileSystemProvider { - // todo@joh -> added, deleted, renamed, changed - onDidChange: Event; - resolveContents(resource: Uri): string | Thenable; - writeContents(resource: Uri, contents: string): void | Thenable; + onDidChange?: Event; - // -- search - // todo@joh - extract into its own provider? - findFiles(query: string, progress: Progress, token?: CancellationToken): Thenable; + root: Uri; + + // more... + // + utimes(resource: Uri, mtime: number): Thenable; + stat(resource: Uri): Thenable; + read(resource: Uri, progress: Progress): Thenable; + write(resource: Uri, content: Uint8Array): Thenable; + unlink(resource: Uri): Thenable; + rename(resource: Uri, target: Uri): Thenable; + mkdir(resource: Uri): Thenable; + readdir(resource: Uri): Thenable; + rmdir(resource: Uri): Thenable; } export namespace workspace { diff --git a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts index 746d242419b..be065d33308 100644 --- a/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/electron-browser/extensionHost.contribution.ts @@ -28,6 +28,7 @@ import './mainThreadEditor'; import './mainThreadEditors'; import './mainThreadErrors'; import './mainThreadExtensionService'; +import './mainThreadFileSystem'; import './mainThreadFileSystemEventService'; import './mainThreadHeapService'; import './mainThreadLanguageFeatures'; diff --git a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts new file mode 100644 index 00000000000..51decb5ec71 --- /dev/null +++ b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { ExtHostContext, MainContext, IExtHostContext, MainThreadFileSystemShape, ExtHostFileSystemShape } from '../node/extHost.protocol'; +import { IFileService, IFileSystemProvider, IStat, IFileChange } from 'vs/platform/files/common/files'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import Event, { Emitter } from 'vs/base/common/event'; +import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; +import { IProgress } from 'vs/platform/progress/common/progress'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; + +@extHostNamedCustomer(MainContext.MainThreadFileSystem) +export class MainThreadFileSystem implements MainThreadFileSystemShape { + + private readonly _toDispose: IDisposable[] = []; + private readonly _proxy: ExtHostFileSystemShape; + private readonly _provider = new Map(); + + constructor( + extHostContext: IExtHostContext, + @IFileService private readonly _fileService: IFileService, + @IWorkspaceEditingService private readonly _workspaceEditService: IWorkspaceEditingService + ) { + this._proxy = extHostContext.get(ExtHostContext.ExtHostFileSystem); + } + + dispose(): void { + dispose(this._toDispose); + } + + $registerFileSystemProvider(handle: number, scheme: string): void { + this._provider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, handle, this._proxy)); + } + + $unregisterFileSystemProvider(handle: number): void { + dispose(this._provider.get(handle)); + this._provider.delete(handle); + } + + $onDidAddFileSystemRoot(uri: URI): void { + this._workspaceEditService.addFolders([uri]); + } + + $onFileSystemChange(handle: number, changes: IFileChange[]): void { + this._provider.get(handle).$onFileSystemChange(changes); + } + + $reportFileChunk(handle: number, resource: URI, chunk: number[]): void { + this._provider.get(handle).reportFileChunk(resource, chunk); + } +} + +class RemoteFileSystemProvider implements IFileSystemProvider { + + private readonly _onDidChange = new Emitter(); + private readonly _registration: IDisposable; + private readonly _reads = new Map>(); + + readonly onDidChange: Event = this._onDidChange.event; + + + constructor( + service: IFileService, + scheme: string, + private readonly _handle: number, + private readonly _proxy: ExtHostFileSystemShape + ) { + this._registration = service.registerProvider(scheme, this); + } + + dispose(): void { + this._registration.dispose(); + this._onDidChange.dispose(); + } + + $onFileSystemChange(changes: IFileChange[]): void { + this._onDidChange.fire(changes); + } + + // --- forwarding calls + + utimes(resource: URI, mtime: number): TPromise { + return this._proxy.$utimes(this._handle, resource, mtime); + } + stat(resource: URI): TPromise { + return this._proxy.$stat(this._handle, resource); + } + read(resource: URI, progress: IProgress): TPromise { + this._reads.set(resource.toString(), progress); + return this._proxy.$read(this._handle, resource); + } + reportFileChunk(resource: URI, chunk: number[]): void { + this._reads.get(resource.toString()).report(Buffer.from(chunk)); + } + write(resource: URI, content: Uint8Array): TPromise { + return this._proxy.$write(this._handle, resource, [].slice.call(content)); + } + unlink(resource: URI): TPromise { + return this._proxy.$unlink(this._handle, resource); + } + rename(resource: URI, target: URI): TPromise { + return this._proxy.$rename(this._handle, resource, target); + } + mkdir(resource: URI): TPromise { + return this._proxy.$mkdir(this._handle, resource); + } + readdir(resource: URI): TPromise { + return this._proxy.$readdir(this._handle, resource); + } + rmdir(resource: URI): TPromise { + return this._proxy.$rmdir(this._handle, resource); + } +} diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index f72611dd369..b8957f1342d 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -6,15 +6,13 @@ import { isPromiseCanceledError } from 'vs/base/common/errors'; import URI from 'vs/base/common/uri'; -import { ISearchService, QueryType, ISearchQuery, ISearchProgressItem, ISearchComplete } from 'vs/platform/search/common/search'; +import { ISearchService, QueryType, ISearchQuery } from 'vs/platform/search/common/search'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { TPromise, PPromise } from 'vs/base/common/winjs.base'; +import { TPromise } from 'vs/base/common/winjs.base'; import { MainThreadWorkspaceShape, ExtHostWorkspaceShape, ExtHostContext, MainContext, IExtHostContext } from '../node/extHost.protocol'; import { IFileService } from 'vs/platform/files/common/files'; -import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; -import { RemoteFileService } from 'vs/workbench/services/files/electron-browser/remoteFileService'; -import { Emitter } from 'vs/base/common/event'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { IExperimentService } from 'vs/platform/telemetry/common/experiments'; @@ -30,7 +28,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { @ISearchService private readonly _searchService: ISearchService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @ITextFileService private readonly _textFileService: ITextFileService, - @IExperimentService private experimentService: IExperimentService, + @IExperimentService private _experimentService: IExperimentService, @IFileService private readonly _fileService: IFileService ) { this._proxy = extHostContext.get(ExtHostContext.ExtHostWorkspace); @@ -65,7 +63,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { maxResults, includePattern: { [include]: true }, excludePattern: { [exclude]: true }, - useRipgrep: this.experimentService.getExperiments().ripgrepQuickSearch + useRipgrep: this._experimentService.getExperiments().ripgrepQuickSearch }; this._searchService.extendQuery(query); @@ -102,81 +100,5 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return result.results.every(each => each.success === true); }); } - - // --- EXPERIMENT: workspace provider - - private _idPool: number = 0; - private readonly _provider = new Map]>(); - private readonly _searchSessions = new Map void, reject: Function, progress: (item: ISearchProgressItem) => void, matches: URI[] }>(); - - $registerFileSystemProvider(handle: number, authority: string): void { - if (!(this._fileService instanceof RemoteFileService)) { - throw new Error(); - } - const emitter = new Emitter(); - const provider = { - onDidChange: emitter.event, - resolve: (resource: URI) => { - return this._proxy.$resolveFile(handle, resource); - }, - update: (resource: URI, value: string) => { - return this._proxy.$storeFile(handle, resource, value); - } - }; - const searchProvider = { - search: (query: ISearchQuery) => { - if (query.type !== QueryType.File) { - return undefined; - } - const session = ++this._idPool; - return new PPromise((resolve, reject, progress) => { - this._searchSessions.set(session, { resolve, reject, progress, matches: [] }); - this._proxy.$startSearch(handle, session, query.filePattern); - }, () => { - this._proxy.$cancelSearch(handle, session); - }); - } - }; - const registrations = combinedDisposable([ - this._fileService.registerProvider(authority, provider), - this._searchService.registerSearchResultProvider(searchProvider), - ]); - this._provider.set(handle, [registrations, emitter]); - } - - $unregisterFileSystemProvider(handle: number): void { - if (this._provider.has(handle)) { - dispose(this._provider.get(handle)[0]); - this._provider.delete(handle); - } - } - - $onFileSystemChange(handle: number, resource: URI) { - const [, emitter] = this._provider.get(handle); - emitter.fire(resource); - }; - - $updateSearchSession(session: number, data: URI): void { - if (this._searchSessions.has(session)) { - this._searchSessions.get(session).progress({ resource: data }); - this._searchSessions.get(session).matches.push(data); - } - } - - $finishSearchSession(session: number, err?: any): void { - if (this._searchSessions.has(session)) { - const { matches, resolve, reject } = this._searchSessions.get(session); - this._searchSessions.delete(session); - if (err) { - reject(err); - } else { - resolve({ - limitHit: false, - stats: undefined, - results: matches.map(resource => ({ resource })) - }); - } - } - } } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index c5707a083c4..a49fe5869f2 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -53,6 +53,8 @@ import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions'; import { ExtHostThreadService } from 'vs/workbench/services/thread/node/extHostThreadService'; import { ProxyIdentifier } from 'vs/workbench/services/thread/common/threadService'; import { ExtHostDialogs } from 'vs/workbench/api/node/extHostDialogs'; +import { ExtHostFileSystem } from 'vs/workbench/api/node/extHostFileSystem'; +import { FileChangeType, FileType } from 'vs/platform/files/common/files'; export interface IExtensionApiFactory { (extension: IExtensionDescription): typeof vscode; @@ -93,6 +95,7 @@ export function createApiFactory( const extHostConfiguration = threadService.set(ExtHostContext.ExtHostConfiguration, new ExtHostConfiguration(threadService.get(MainContext.MainThreadConfiguration), extHostWorkspace, initData.configuration)); const extHostDiagnostics = threadService.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(threadService)); const languageFeatures = threadService.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(threadService, extHostDocuments, extHostCommands, extHostHeapService, extHostDiagnostics)); + const extHostFileSystem = threadService.set(ExtHostContext.ExtHostFileSystem, new ExtHostFileSystem(threadService)); const extHostFileSystemEvent = threadService.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService()); const extHostQuickOpen = threadService.set(ExtHostContext.ExtHostQuickOpen, new ExtHostQuickOpen(threadService, extHostWorkspace, extHostCommands)); const extHostTerminalService = threadService.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(threadService)); @@ -480,7 +483,7 @@ export function createApiFactory( return extHostTask.registerTaskProvider(extension, provider); }, registerFileSystemProvider: proposedApiFunction(extension, (authority, provider) => { - return extHostWorkspace.registerFileSystemProvider(authority, provider); + return extHostFileSystem.registerFileSystemProvider(authority, provider); }) }; @@ -604,7 +607,11 @@ export function createApiFactory( ShellExecution: extHostTypes.ShellExecution, TaskScope: extHostTypes.TaskScope, Task: extHostTypes.Task, - ConfigurationTarget: extHostTypes.ConfigurationTarget + ConfigurationTarget: extHostTypes.ConfigurationTarget, + + // TODO@JOH + FileChangeType: FileChangeType, + FileType: FileType }; if (extension.enableProposedApi && extension.isBuiltin) { api['credentials'] = credentials; diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 20dc029fe9a..b2c0b08a943 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -48,6 +48,7 @@ import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { IDisposable } from 'vs/base/common/lifecycle'; import { SerializedError } from 'vs/base/common/errors'; import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IStat, IFileChange } from 'vs/platform/files/common/files'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -311,12 +312,15 @@ export interface MainThreadWorkspaceShape extends IDisposable { $startSearch(include: string, exclude: string, maxResults: number, requestId: number): Thenable; $cancelSearch(requestId: number): Thenable; $saveAll(includeUntitled?: boolean): Thenable; +} - $registerFileSystemProvider(handle: number, authority: string): void; +export interface MainThreadFileSystemShape extends IDisposable { + $registerFileSystemProvider(handle: number, scheme: string): void; $unregisterFileSystemProvider(handle: number): void; - $onFileSystemChange(handle: number, resource: URI): void; - $updateSearchSession(session: number, data): void; - $finishSearchSession(session: number, err?: any): void; + + $onDidAddFileSystemRoot(root: URI): void; + $onFileSystemChange(handle: number, resource: IFileChange[]): void; + $reportFileChunk(handle: number, resource: URI, chunk: number[] | null): void; } export interface MainThreadTaskShape extends IDisposable { @@ -470,11 +474,18 @@ export interface ExtHostTreeViewsShape { export interface ExtHostWorkspaceShape { $acceptWorkspaceData(workspace: IWorkspaceData): void; +} - $resolveFile(handle: number, resource: URI): TPromise; - $storeFile(handle: number, resource: URI, content: string): TPromise; - $startSearch(handle: number, session: number, query: string): void; - $cancelSearch(handle: number, session: number): void; +export interface ExtHostFileSystemShape { + $utimes(handle: number, resource: URI, mtime: number): TPromise; + $stat(handle: number, resource: URI): TPromise; + $read(handle: number, resource: URI): TPromise; + $write(handle: number, resource: URI, content: number[]): TPromise; + $unlink(handle: number, resource: URI): TPromise; + $rename(handle: number, resource: URI, target: URI): TPromise; + $mkdir(handle: number, resource: URI): TPromise; + $readdir(handle: number, resource: URI): TPromise; + $rmdir(handle: number, resource: URI): TPromise; } export interface ExtHostExtensionServiceShape { @@ -611,6 +622,7 @@ export const MainContext = { MainThreadTelemetry: createMainId('MainThreadTelemetry'), MainThreadTerminalService: createMainId('MainThreadTerminalService'), MainThreadWorkspace: createMainId('MainThreadWorkspace'), + MainThreadFileSystem: createMainId('MainThreadFileSystem'), MainThreadExtensionService: createMainId('MainThreadExtensionService'), MainThreadSCM: createMainId('MainThreadSCM'), MainThreadTask: createMainId('MainThreadTask'), @@ -629,6 +641,7 @@ export const ExtHostContext = { ExtHostDocumentSaveParticipant: createExtId('ExtHostDocumentSaveParticipant'), ExtHostEditors: createExtId('ExtHostEditors'), ExtHostTreeViews: createExtId('ExtHostTreeViews'), + ExtHostFileSystem: createExtId('ExtHostFileSystem'), ExtHostFileSystemEventService: createExtId('ExtHostFileSystemEventService'), ExtHostHeapService: createExtId('ExtHostHeapMonitor'), ExtHostLanguageFeatures: createExtId('ExtHostLanguageFeatures'), diff --git a/src/vs/workbench/api/node/extHostFileSystem.ts b/src/vs/workbench/api/node/extHostFileSystem.ts new file mode 100644 index 00000000000..d46e8ac3f3a --- /dev/null +++ b/src/vs/workbench/api/node/extHostFileSystem.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { MainContext, IMainContext, ExtHostFileSystemShape, MainThreadFileSystemShape } from './extHost.protocol'; +import * as vscode from 'vscode'; +import { IStat } from 'vs/platform/files/common/files'; +import { IDisposable } from 'vs/base/common/lifecycle'; + +export class ExtHostFileSystem implements ExtHostFileSystemShape { + + private readonly _proxy: MainThreadFileSystemShape; + private readonly _provider = new Map(); + private _handlePool: number = 0; + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.get(MainContext.MainThreadFileSystem); + } + + registerFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider) { + const handle = this._handlePool++; + this._provider.set(handle, provider); + this._proxy.$registerFileSystemProvider(handle, scheme); + this._proxy.$onDidAddFileSystemRoot(provider.root); + let reg: IDisposable; + if (provider.onDidChange) { + reg = provider.onDidChange(event => this._proxy.$onFileSystemChange(handle, event)); + } + return { + dispose: () => { + if (reg) { + reg.dispose(); + } + this._provider.delete(handle); + this._proxy.$unregisterFileSystemProvider(handle); + } + }; + } + + $utimes(handle: number, resource: URI, mtime: number): TPromise { + return TPromise.as(this._provider.get(handle).utimes(resource, mtime)); + } + $stat(handle: number, resource: URI): TPromise { + return TPromise.as(this._provider.get(handle).stat(resource)); + } + $read(handle: number, resource: URI): TPromise { + return TPromise.as(this._provider.get(handle).read(resource, { + report: (chunk) => { + this._proxy.$reportFileChunk(handle, resource, [].slice.call(chunk)); + } + })); + } + $write(handle: number, resource: URI, content: number[]): TPromise { + return TPromise.as(this._provider.get(handle).write(resource, Buffer.from(content))); + } + $unlink(handle: number, resource: URI): TPromise { + return TPromise.as(this._provider.get(handle).unlink(resource)); + } + $rename(handle: number, resource: URI, target: URI): TPromise { + return TPromise.as(this._provider.get(handle).rename(resource, target)); + } + $mkdir(handle: number, resource: URI): TPromise { + return TPromise.as(this._provider.get(handle).mkdir(resource)); + } + $readdir(handle: number, resource: URI): TPromise { + return TPromise.as(this._provider.get(handle).readdir(resource)); + } + $rmdir(handle: number, resource: URI): TPromise { + return TPromise.as(this._provider.get(handle).rmdir(resource)); + } +} diff --git a/src/vs/workbench/api/node/extHostWorkspace.ts b/src/vs/workbench/api/node/extHostWorkspace.ts index 629947ee2a5..a87f6751751 100644 --- a/src/vs/workbench/api/node/extHostWorkspace.ts +++ b/src/vs/workbench/api/node/extHostWorkspace.ts @@ -10,15 +10,10 @@ import { normalize } from 'vs/base/common/paths'; import { delta } from 'vs/base/common/arrays'; import { relative } from 'path'; import { Workspace } from 'vs/platform/workspace/common/workspace'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IWorkspaceData, ExtHostWorkspaceShape, MainContext, MainThreadWorkspaceShape, IMainContext } from './extHost.protocol'; import * as vscode from 'vscode'; import { compare } from 'vs/base/common/strings'; -import { asWinJsPromise } from 'vs/base/common/async'; -import { Disposable } from 'vs/workbench/api/node/extHostTypes'; import { TrieMap } from 'vs/base/common/map'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Progress } from 'vs/platform/progress/common/progress'; class Workspace2 extends Workspace { @@ -179,52 +174,4 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape { saveAll(includeUntitled?: boolean): Thenable { return this._proxy.$saveAll(includeUntitled); } - - // --- EXPERIMENT: workspace resolver - - - private _handlePool = 0; - private readonly _fsProvider = new Map(); - private readonly _searchSession = new Map(); - - registerFileSystemProvider(authority: string, provider: vscode.FileSystemProvider): vscode.Disposable { - const handle = ++this._handlePool; - this._fsProvider.set(handle, provider); - const reg = provider.onDidChange(e => this._proxy.$onFileSystemChange(handle, e)); - this._proxy.$registerFileSystemProvider(handle, authority); - return new Disposable(() => { - this._fsProvider.delete(handle); - reg.dispose(); - }); - } - - $resolveFile(handle: number, resource: URI): TPromise { - const provider = this._fsProvider.get(handle); - return asWinJsPromise(token => provider.resolveContents(resource)); - } - - $storeFile(handle: number, resource: URI, content: string): TPromise { - const provider = this._fsProvider.get(handle); - return asWinJsPromise(token => provider.writeContents(resource, content)); - } - - $startSearch(handle: number, session: number, query: string): void { - const provider = this._fsProvider.get(handle); - const source = new CancellationTokenSource(); - const progress = new Progress(chunk => this._proxy.$updateSearchSession(session, chunk)); - - this._searchSession.set(session, source); - TPromise.wrap(provider.findFiles(query, progress, source.token)).then(() => { - this._proxy.$finishSearchSession(session); - }, err => { - this._proxy.$finishSearchSession(session, err); - }); - } - - $cancelSearch(handle: number, session: number): void { - if (this._searchSession.has(session)) { - this._searchSession.get(session).cancel(); - this._searchSession.delete(session); - } - } } diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 20399881630..244b7a49abd 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -7,6 +7,7 @@ import uri from 'vs/base/common/uri'; import paths = require('vs/base/common/paths'); +import resources = require('vs/base/common/resources'); import { IconLabel, IIconLabelOptions, IIconLabelCreationOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -147,7 +148,7 @@ export class ResourceLabel extends IconLabel { if (this.options && typeof this.options.title === 'string') { title = this.options.title; } else if (resource) { - title = getPathLabel(resource.fsPath, void 0, this.environmentService); + title = getPathLabel(resource, void 0, this.environmentService); } if (!this.computedIconClasses) { @@ -225,10 +226,10 @@ export class FileLabel extends ResourceLabel { } } - let description: string; + const hidePath = (options && options.hidePath) || (resource.scheme === Schemas.untitled && !this.untitledEditorService.hasAssociatedFilePath(resource)); + let rootProvider: IWorkspaceFolderProvider; if (!hidePath) { - let rootProvider: IWorkspaceFolderProvider; if (options && options.root) { rootProvider = { getWorkspaceFolder(): { uri } { return { uri: options.root }; }, @@ -237,11 +238,13 @@ export class FileLabel extends ResourceLabel { } else { rootProvider = this.contextService; } - - description = getPathLabel(paths.dirname(resource.fsPath), rootProvider, this.environmentService); } - this.setLabel({ resource, name, description }, options); + this.setLabel({ + resource, + name: (options && options.hideLabel) ? void 0 : resources.basenameOrAuthority(resource), + description: !hidePath ? getPathLabel(resources.dirname(resource), rootProvider, this.environmentService) : void 0 + }, options); } } @@ -250,34 +253,30 @@ export function getIconClasses(modelService: IModelService, modeService: IModeSe // we always set these base classes even if we do not have a path const classes = fileKind === FileKind.ROOT_FOLDER ? ['rootfolder-icon'] : fileKind === FileKind.FOLDER ? ['folder-icon'] : ['file-icon']; - let path: string; - if (resource) { - path = resource.fsPath; - } - if (path) { - const basename = cssEscape(paths.basename(path).toLowerCase()); + if (resource) { + const name = cssEscape(resources.basenameOrAuthority(resource).toLowerCase()); // Folders if (fileKind === FileKind.FOLDER) { - classes.push(`${basename}-name-folder-icon`); + classes.push(`${name}-name-folder-icon`); } // Files else { // Name - classes.push(`${basename}-name-file-icon`); + classes.push(`${name}-name-file-icon`); // Extension(s) - const dotSegments = basename.split('.'); + const dotSegments = name.split('.'); for (let i = 1; i < dotSegments.length; i++) { classes.push(`${dotSegments.slice(i).join('.')}-ext-file-icon`); // add each combination of all found extensions if more than one } // Configured Language let configuredLangId = getConfiguredLangId(modelService, resource); - configuredLangId = configuredLangId || modeService.getModeIdByFilenameOrFirstLine(path); + configuredLangId = configuredLangId || modeService.getModeIdByFilenameOrFirstLine(name); if (configuredLangId) { classes.push(`${cssEscape(configuredLangId)}-lang-file-icon`); } @@ -303,4 +302,4 @@ function getConfiguredLangId(modelService: IModelService, resource: uri): string function cssEscape(val: string): string { return val.replace(/\s/g, '\\$&'); // make sure to not introduce CSS classes from files that contain whitespace -} \ No newline at end of file +} diff --git a/src/vs/workbench/common/editor/untitledEditorInput.ts b/src/vs/workbench/common/editor/untitledEditorInput.ts index 8ce430fb0f5..dac7180d617 100644 --- a/src/vs/workbench/common/editor/untitledEditorInput.ts +++ b/src/vs/workbench/common/editor/untitledEditorInput.ts @@ -7,9 +7,11 @@ import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import { suggestFilename } from 'vs/base/common/mime'; +import { memoize } from 'vs/base/common/decorators'; import labels = require('vs/base/common/labels'); import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import paths = require('vs/base/common/paths'); +import resources = require('vs/base/common/resources'); import { EditorInput, IEncodingSupport, EncodingMode, ConfirmResult } from 'vs/workbench/common/editor'; import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -37,14 +39,6 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport private toUnbind: IDisposable[]; - private shortDescription: string; - private mediumDescription: string; - private longDescription: string; - - private shortTitle: string; - private mediumTitle: string; - private longTitle: string; - constructor( private resource: URI, hasAssociatedFilePath: boolean, @@ -94,7 +88,22 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport } public getName(): string { - return this.hasAssociatedFilePath ? paths.basename(this.resource.fsPath) : this.resource.fsPath; + return this.hasAssociatedFilePath ? resources.basenameOrAuthority(this.resource) : this.resource.path; + } + + @memoize + private get shortDescription(): string { + return paths.basename(labels.getPathLabel(resources.dirname(this.resource), void 0, this.environmentService)); + } + + @memoize + private get mediumDescription(): string { + return labels.getPathLabel(resources.dirname(this.resource), this.contextService, this.environmentService); + } + + @memoize + private get longDescription(): string { + return labels.getPathLabel(resources.dirname(this.resource), void 0, this.environmentService); } public getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string { @@ -105,20 +114,35 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport let description: string; switch (verbosity) { case Verbosity.SHORT: - description = this.shortDescription ? this.shortDescription : (this.shortDescription = paths.basename(labels.getPathLabel(paths.dirname(this.resource.fsPath), void 0, this.environmentService))); + description = this.shortDescription; break; case Verbosity.LONG: - description = this.longDescription ? this.longDescription : (this.longDescription = labels.getPathLabel(paths.dirname(this.resource.fsPath), void 0, this.environmentService)); + description = this.longDescription; break; case Verbosity.MEDIUM: default: - description = this.mediumDescription ? this.mediumDescription : (this.mediumDescription = labels.getPathLabel(paths.dirname(this.resource.fsPath), this.contextService, this.environmentService)); + description = this.mediumDescription; break; } return description; } + @memoize + private get shortTitle(): string { + return this.getName(); + } + + @memoize + private get mediumTitle(): string { + return labels.getPathLabel(this.resource, this.contextService, this.environmentService); + } + + @memoize + private get longTitle(): string { + return labels.getPathLabel(this.resource, void 0, this.environmentService); + } + public getTitle(verbosity: Verbosity): string { if (!this.hasAssociatedFilePath) { return this.getName(); @@ -127,13 +151,13 @@ export class UntitledEditorInput extends EditorInput implements IEncodingSupport let title: string; switch (verbosity) { case Verbosity.SHORT: - title = this.shortTitle ? this.shortTitle : (this.shortTitle = this.getName()); + title = this.shortTitle; break; case Verbosity.MEDIUM: - title = this.mediumTitle ? this.mediumTitle : (this.mediumTitle = labels.getPathLabel(this.resource, this.contextService, this.environmentService)); + title = this.mediumTitle; break; case Verbosity.LONG: - title = this.longTitle ? this.longTitle : (this.longTitle = labels.getPathLabel(this.resource, void 0, this.environmentService)); + title = this.longTitle; break; } diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 8133a94c1b8..1917de3e094 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -554,6 +554,11 @@ export class Workbench implements IPartService { this.toShutdown.push(this.activitybarPart); serviceCollection.set(IActivityBarService, this.activitybarPart); + // File Service + this.fileService = this.instantiationService.createInstance(RemoteFileService); + serviceCollection.set(IFileService, this.fileService); + this.toDispose.push(this.fileService.onFileChanges(e => this.configurationService.handleWorkspaceFileEvents(e))); + // Editor service (editor part) this.editorPart = this.instantiationService.createInstance(EditorPart, Identifiers.EDITOR_PART, !this.hasFilesToCreateOpenOrDiff); this.toDispose.push(this.editorPart); @@ -568,11 +573,6 @@ export class Workbench implements IPartService { this.toShutdown.push(this.titlebarPart); serviceCollection.set(ITitleService, this.titlebarPart); - // File Service - this.fileService = this.instantiationService.createInstance(RemoteFileService); - serviceCollection.set(IFileService, this.fileService); - this.toDispose.push(this.fileService.onFileChanges(e => this.configurationService.handleWorkspaceFileEvents(e))); - // History serviceCollection.set(IHistoryService, new SyncDescriptor(HistoryService)); diff --git a/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts b/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts index 30ef13ec313..36a53527512 100644 --- a/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts +++ b/src/vs/workbench/parts/execution/electron-browser/execution.contribution.ts @@ -13,6 +13,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import paths = require('vs/base/common/paths'); +import resources = require('vs/base/common/resources'); import { Scope, IActionBarRegistry, Extensions as ActionBarExtensions, ActionBarContributor } from 'vs/workbench/browser/actions'; import uri from 'vs/base/common/uri'; import { explorerItemToFileResource } from 'vs/workbench/parts/files/common/files'; @@ -182,7 +183,8 @@ export class ExplorerViewerActionContributor extends ActionBarContributor { } public hasSecondaryActions(context: any): boolean { - return !!explorerItemToFileResource(context.element); + const fileResource = explorerItemToFileResource(context.element); + return fileResource && fileResource.resource.scheme === 'file'; } public getSecondaryActions(context: any): IAction[] { @@ -191,7 +193,7 @@ export class ExplorerViewerActionContributor extends ActionBarContributor { // We want the parent unless this resource is a directory if (!fileResource.isDirectory) { - resource = uri.file(paths.dirname(resource.fsPath)); + resource = resources.dirname(resource); } const configuration = this.configurationService.getConfiguration(); diff --git a/src/vs/workbench/parts/files/browser/fileActions.contribution.ts b/src/vs/workbench/parts/files/browser/fileActions.contribution.ts index c56992637f1..4eeeacc0311 100644 --- a/src/vs/workbench/parts/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/parts/files/browser/fileActions.contribution.ts @@ -166,18 +166,17 @@ class ExplorerViewersActionContributor extends ActionBarContributor { public getSecondaryActions(context: any): IAction[] { const actions: IAction[] = []; + const fileResource = explorerItemToFileResource(context.element); + const resource = fileResource.resource; - if (this.hasSecondaryActions(context)) { - const fileResource = explorerItemToFileResource(context.element); - const resource = fileResource.resource; - - // Reveal file in OS native explorer + // Reveal file in OS native explorer + if (resource.scheme === 'file') { actions.push(this.instantiationService.createInstance(RevealInOSAction, resource)); - - // Copy Path - actions.push(this.instantiationService.createInstance(CopyPathAction, resource)); } + // Copy Path + actions.push(this.instantiationService.createInstance(CopyPathAction, resource)); + return actions; } } diff --git a/src/vs/workbench/parts/files/browser/fileActions.ts b/src/vs/workbench/parts/files/browser/fileActions.ts index 3c609e62030..30b6d67ec62 100644 --- a/src/vs/workbench/parts/files/browser/fileActions.ts +++ b/src/vs/workbench/parts/files/browser/fileActions.ts @@ -11,6 +11,7 @@ import nls = require('vs/nls'); import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; import { sequence, ITask } from 'vs/base/common/async'; import paths = require('vs/base/common/paths'); +import resources = require('vs/base/common/resources'); import URI from 'vs/base/common/uri'; import errors = require('vs/base/common/errors'); import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -296,22 +297,20 @@ class RenameFileAction extends BaseRenameAction { } public runAction(newName: string): TPromise { - - const dirty = this.textFileService.getDirty().filter(d => paths.isEqualOrParent(d.fsPath, this.element.resource.fsPath, !isLinux /* ignorecase */)); + const dirty = this.textFileService.getDirty().filter(d => resources.isEqualOrParent(d, this.element.resource, !isLinux /* ignorecase */)); const dirtyRenamed: URI[] = []; return TPromise.join(dirty.map(d => { - - const targetPath = paths.join(this.element.parent.resource.fsPath, newName); let renamed: URI; // If the dirty file itself got moved, just reparent it to the target folder - if (paths.isEqual(this.element.resource.fsPath, d.fsPath)) { - renamed = URI.file(targetPath); + const targetPath = paths.join(this.element.parent.resource.path, newName); + if (this.element.resource.toString() === d.toString()) { + renamed = this.element.parent.resource.with({ path: targetPath }); } // Otherwise, a parent of the dirty resource got moved, so we have to reparent more complicated. Example: else { - renamed = URI.file(paths.join(targetPath, d.fsPath.substr(this.element.resource.fsPath.length + 1))); + renamed = this.element.parent.resource.with({ path: paths.join(targetPath, d.path.substr(this.element.resource.path.length + 1)) }); } dirtyRenamed.push(renamed); @@ -589,7 +588,8 @@ export class CreateFileAction extends BaseCreateAction { } public runAction(fileName: string): TPromise { - return this.fileService.createFile(URI.file(paths.join(this.element.parent.resource.fsPath, fileName))).then(stat => { + const resource = this.element.parent.resource; + return this.fileService.createFile(resource.with({ path: paths.join(resource.path, fileName) })).then(stat => { return this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); }, (error) => { this.onErrorWithRetry(error, () => this.runAction(fileName)); @@ -615,7 +615,8 @@ export class CreateFolderAction extends BaseCreateAction { } public runAction(fileName: string): TPromise { - return this.fileService.createFolder(URI.file(paths.join(this.element.parent.resource.fsPath, fileName))).then(null, (error) => { + const resource = this.element.parent.resource; + return this.fileService.createFolder(resource.with({ path: paths.join(resource.path, fileName) })).then(null, (error) => { this.onErrorWithRetry(error, () => this.runAction(fileName)); }); } @@ -673,7 +674,7 @@ export class BaseDeleteFileAction extends BaseFileAction { // Handle dirty let revertPromise: TPromise = TPromise.as(null); - const dirty = this.textFileService.getDirty().filter(d => paths.isEqualOrParent(d.fsPath, this.element.resource.fsPath, !isLinux /* ignorecase */)); + const dirty = this.textFileService.getDirty().filter(d => resources.isEqualOrParent(d, this.element.resource, !isLinux /* ignorecase */)); if (dirty.length) { let message: string; if (this.element.isDirectory) { @@ -798,10 +799,9 @@ export class ImportFileAction extends BaseFileAction { return this.tree; } - public run(context?: any): TPromise { + public run(resources: URI[]): TPromise { const importPromise = TPromise.as(null).then(() => { - const input = context.input as { paths: string[] }; - if (input.paths && input.paths.length > 0) { + if (resources && resources.length > 0) { // Find parent for import let targetElement: FileStat; @@ -826,8 +826,8 @@ export class ImportFileAction extends BaseFileAction { }); let overwrite = true; - if (input.paths.some(path => { - return !!targetNames[isLinux ? paths.basename(path) : paths.basename(path).toLowerCase()]; + if (resources.some(resource => { + return !!targetNames[isLinux ? paths.basename(resource.fsPath) : paths.basename(resource.fsPath).toLowerCase()]; })) { const confirm: IConfirmation = { message: nls.localize('confirmOverwrite', "A file or folder with the same name already exists in the destination folder. Do you want to replace it?"), @@ -845,10 +845,10 @@ export class ImportFileAction extends BaseFileAction { // Run import in sequence const importPromisesFactory: ITask>[] = []; - input.paths.forEach(path => { + resources.forEach(resource => { importPromisesFactory.push(() => { - const sourceFile = URI.file(path); - const targetFile = URI.file(paths.join(targetElement.resource.fsPath, paths.basename(path))); + const sourceFile = resource; + const targetFile = targetElement.resource.with({ path: paths.join(targetElement.resource.path, paths.basename(sourceFile.path)) }); // if the target exists and is dirty, make sure to revert it. otherwise the dirty contents // of the target file would replace the contents of the imported file. since we already @@ -862,7 +862,7 @@ export class ImportFileAction extends BaseFileAction { return this.fileService.importFile(sourceFile, targetElement.resource).then(res => { // if we only import one file, just open it directly - if (input.paths.length === 1) { + if (resources.length === 1) { this.editorService.openEditor({ resource: res.stat.resource, options: { pinned: true } }).done(null, errors.onUnexpectedError); } }, error => this.onError(error)); @@ -964,7 +964,7 @@ export class PasteFileAction extends BaseFileAction { } // Check if target is ancestor of pasted folder - if (!paths.isEqual(this.element.resource.fsPath, fileToCopy.resource.fsPath) && paths.isEqualOrParent(this.element.resource.fsPath, fileToCopy.resource.fsPath, !isLinux /* ignorecase */)) { + if (this.element.resource.toString() !== fileToCopy.resource.toString() && resources.isEqualOrParent(this.element.resource, fileToCopy.resource, !isLinux /* ignorecase */)) { return false; } @@ -1047,14 +1047,14 @@ export class DuplicateFileAction extends BaseFileAction { private findTarget(): URI { let name = this.element.name; - let candidate = URI.file(paths.join(this.target.resource.fsPath, name)); + let candidate = this.target.resource.with({ path: paths.join(this.target.resource.fsPath, name) }); while (true) { if (!this.element.root.find(candidate)) { break; } name = this.toCopyName(name, this.element.isDirectory); - candidate = URI.file(paths.join(this.target.resource.fsPath, name)); + candidate = this.target.resource.with({ path: paths.join(this.target.resource.fsPath, name) }); } return candidate; @@ -1210,7 +1210,7 @@ export class GlobalCompareResourcesAction extends Action { } label = paths.basename(resource.fsPath); - description = resource.scheme === 'file' ? labels.getPathLabel(paths.dirname(resource.fsPath), this.contextService, this.environmentService) : void 0; + description = labels.getPathLabel(resources.dirname(resource), this.contextService, this.environmentService); return { input, resource, label, description }; }).filter(p => !!p); @@ -1370,7 +1370,8 @@ export abstract class BaseSaveOneFileAction extends BaseSaveFileAction { if (this.resource) { source = this.resource; } else { - source = toResource(this.editorService.getActiveEditorInput(), { supportSideBySide: true, filter: ['file', 'untitled'] }); + // source = toResource(this.editorService.getActiveEditorInput(), { supportSideBySide: true, filter: ['file', 'untitled'] }); + source = toResource(this.editorService.getActiveEditorInput(), { supportSideBySide: true }); } if (source) { diff --git a/src/vs/workbench/parts/files/browser/fileCommands.ts b/src/vs/workbench/parts/files/browser/fileCommands.ts index f33e0388704..157239a2b27 100644 --- a/src/vs/workbench/parts/files/browser/fileCommands.ts +++ b/src/vs/workbench/parts/files/browser/fileCommands.ts @@ -22,7 +22,6 @@ import { FileStat, OpenEditor } from 'vs/workbench/parts/files/common/explorerMo import errors = require('vs/base/common/errors'); import { ITree } from 'vs/base/parts/tree/browser/tree'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import labels = require('vs/base/common/labels'); import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { IMessageService } from 'vs/platform/message/common/message'; @@ -44,7 +43,7 @@ export const copyPathCommand = (accessor: ServicesAccessor, resource?: URI) => { if (resource) { const clipboardService = accessor.get(IClipboardService); - clipboardService.writeText(labels.getPathLabel(resource)); + clipboardService.writeText(resource.scheme === 'file' ? resource.fsPath : resource.toString()); } else { const messageService = accessor.get(IMessageService); messageService.show(severity.Info, nls.localize('openFileToCopy', "Open a file first to copy its path")); diff --git a/src/vs/workbench/parts/files/browser/views/explorerView.ts b/src/vs/workbench/parts/files/browser/views/explorerView.ts index b6e37336889..b2b8d890596 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerView.ts @@ -10,8 +10,8 @@ import { Builder, $ } from 'vs/base/browser/builder'; import URI from 'vs/base/common/uri'; import { ThrottledDelayer } from 'vs/base/common/async'; import errors = require('vs/base/common/errors'); -import labels = require('vs/base/common/labels'); import paths = require('vs/base/common/paths'); +import resources = require('vs/base/common/resources'); import glob = require('vs/base/common/glob'); import { Action, IAction } from 'vs/base/common/actions'; import { prepareActions } from 'vs/workbench/browser/actions'; @@ -133,7 +133,7 @@ export class ExplorerView extends CollapsibleView { const titleSpan = $('span').appendTo(titleDiv); const setHeader = () => { const workspace = this.contextService.getWorkspace(); - const title = workspace.folders.map(folder => labels.getPathLabel(folder.uri.fsPath, void 0, this.environmentService)).join(); + const title = workspace.folders.map(folder => folder.name).join(); titleSpan.text(this.name).title(title); }; this.toDispose.push(this.contextService.onDidChangeWorkspaceName(() => setHeader())); @@ -460,7 +460,7 @@ export class ExplorerView extends CollapsibleView { // Add if (e.operation === FileOperation.CREATE || e.operation === FileOperation.IMPORT || e.operation === FileOperation.COPY) { const addedElement = e.target; - const parentResource = URI.file(paths.dirname(addedElement.resource.fsPath)); + const parentResource = resources.dirname(addedElement.resource); const parents = this.model.findAll(parentResource); if (parents.length) { @@ -495,8 +495,8 @@ export class ExplorerView extends CollapsibleView { const oldResource = e.resource; const newElement = e.target; - const oldParentResource = URI.file(paths.dirname(oldResource.fsPath)); - const newParentResource = URI.file(paths.dirname(newElement.resource.fsPath)); + const oldParentResource = resources.dirname(oldResource); + const newParentResource = resources.dirname(newElement.resource); // Only update focus if renamed/moved element is selected let restoreFocus = false; @@ -770,7 +770,7 @@ export class ExplorerView extends CollapsibleView { return FileStat.create({ resource: targetsToResolve[index].resource, - name: paths.basename(targetsToResolve[index].resource.fsPath), + name: resources.basenameOrAuthority(targetsToResolve[index].resource), mtime: 0, etag: undefined, isDirectory: true, @@ -806,7 +806,7 @@ export class ExplorerView extends CollapsibleView { // Drop those path which are parents of the current one for (let i = resolvedDirectories.length - 1; i >= 0; i--) { const resource = resolvedDirectories[i]; - if (paths.isEqualOrParent(stat.resource.fsPath, resource.fsPath, !isLinux /* ignorecase */)) { + if (resources.isEqualOrParent(stat.resource, resource, !isLinux /* ignorecase */)) { resolvedDirectories.splice(i); } } diff --git a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts index 960e7d8dd8a..a98fe094684 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts @@ -13,6 +13,7 @@ import URI from 'vs/base/common/uri'; import { MIME_BINARY } from 'vs/base/common/mime'; import { once } from 'vs/base/common/functional'; import paths = require('vs/base/common/paths'); +import resources = require('vs/base/common/resources'); import errors = require('vs/base/common/errors'); import { isString } from 'vs/base/common/types'; import { IAction, ActionRunner as BaseActionRunner, IActionRunner } from 'vs/base/common/actions'; @@ -350,9 +351,9 @@ export class FileRenderer implements IRenderer { }); const styler = attachInputBoxStyler(inputBox, this.themeService); - const parent = paths.dirname(stat.resource.fsPath); + const parent = resources.dirname(stat.resource); inputBox.onDidChange(value => { - label.setFile(URI.file(paths.join(parent, value)), labelOptions); // update label icon while typing! + label.setFile(parent.with({ path: paths.join(parent.path, value) }), labelOptions); // update label icon while typing! }); const value = stat.name || ''; @@ -752,7 +753,7 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { } if (stat.isDirectory) { - return URI.from({ scheme: 'folder', path: stat.resource.fsPath }); // indicates that we are dragging a folder + return URI.from({ scheme: 'folder', path: stat.resource.path }); // indicates that we are dragging a folder } return stat.resource; @@ -836,11 +837,11 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { return true; // Can not move anything onto itself } - if (!isCopy && paths.isEqual(paths.dirname(source.resource.fsPath), target.resource.fsPath)) { + if (!isCopy && resources.dirname(source.resource).toString() === target.resource.toString()) { return true; // Can not move a file to the same parent unless we copy } - if (paths.isEqualOrParent(target.resource.fsPath, source.resource.fsPath, !isLinux /* ignorecase */)) { + if (resources.isEqualOrParent(target.resource, source.resource, !isLinux /* ignorecase */)) { return true; // Can not move a parent folder into one of its children } @@ -928,9 +929,8 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { // Handle dropped files (only support FileStat as target) else if (target instanceof FileStat) { const importAction = this.instantiationService.createInstance(ImportFileAction, tree, target, null); - return importAction.run({ - input: { paths: droppedResources.map(res => res.resource.fsPath) } - }); + + return importAction.run(droppedResources.map(res => res.resource)); } return void 0; @@ -962,18 +962,18 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { }; // 1. check for dirty files that are being moved and backup to new target - const dirty = this.textFileService.getDirty().filter(d => paths.isEqualOrParent(d.fsPath, source.resource.fsPath, !isLinux /* ignorecase */)); + const dirty = this.textFileService.getDirty().filter(d => resources.isEqualOrParent(d, source.resource, !isLinux /* ignorecase */)); return TPromise.join(dirty.map(d => { let moved: URI; // If the dirty file itself got moved, just reparent it to the target folder - if (paths.isEqual(source.resource.fsPath, d.fsPath)) { - moved = URI.file(paths.join(target.resource.fsPath, source.name)); + if (source.resource.toString() === d.toString()) { + moved = target.resource.with({ path: paths.join(target.resource.path, source.name) }); } // Otherwise, a parent of the dirty resource got moved, so we have to reparent more complicated. Example: else { - moved = URI.file(paths.join(target.resource.fsPath, d.fsPath.substr(source.parent.resource.fsPath.length + 1))); + moved = target.resource.with({ path: paths.join(target.resource.path, d.path.substr(source.parent.resource.path.length + 1)) }); } dirtyMoved.push(moved); @@ -988,7 +988,7 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { // 3.) run the move operation .then(() => { - const targetResource = URI.file(paths.join(target.resource.fsPath, source.name)); + const targetResource = target.resource.with({ path: paths.join(target.resource.path, source.name) }); let didHandleConflict = false; return this.fileService.moveFile(source.resource, targetResource).then(null, error => { @@ -1006,7 +1006,7 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { // Move with overwrite if the user confirms if (this.messageService.confirm(confirm)) { - const targetDirty = this.textFileService.getDirty().filter(d => paths.isEqualOrParent(d.fsPath, targetResource.fsPath, !isLinux /* ignorecase */)); + const targetDirty = this.textFileService.getDirty().filter(d => resources.isEqualOrParent(d, targetResource, !isLinux /* ignorecase */)); // Make sure to revert all dirty in target first to be able to overwrite properly return this.textFileService.revertAll(targetDirty, { soft: true /* do not attempt to load content from disk */ }).then(() => { diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts index 1393994840d..d3930c9fcff 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts @@ -6,7 +6,9 @@ import { localize } from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; +import { memoize } from 'vs/base/common/decorators'; import paths = require('vs/base/common/paths'); +import resources = require('vs/base/common/resources'); import labels = require('vs/base/common/labels'); import URI from 'vs/base/common/uri'; import { EncodingMode, ConfirmResult, EditorInput, IFileEditorInput, ITextEditorModel } from 'vs/workbench/common/editor'; @@ -33,14 +35,6 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { private name: string; - private shortDescription: string; - private mediumDescription: string; - private longDescription: string; - - private shortTitle: string; - private mediumTitle: string; - private longTitle: string; - private toUnbind: IDisposable[]; /** @@ -124,41 +118,72 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { public getName(): string { if (!this.name) { - this.name = paths.basename(this.resource.fsPath); + this.name = resources.basenameOrAuthority(this.resource); } return this.decorateOrphanedFiles(this.name); } + @memoize + private get shortDescription(): string { + + return paths.basename(labels.getPathLabel(resources.dirname(this.resource), void 0, this.environmentService)); + } + + @memoize + private get mediumDescription(): string { + return labels.getPathLabel(resources.dirname(this.resource), this.contextService, this.environmentService); + } + + @memoize + private get longDescription(): string { + return labels.getPathLabel(resources.dirname(this.resource), void 0, this.environmentService); + } + public getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string { let description: string; switch (verbosity) { case Verbosity.SHORT: - description = this.shortDescription ? this.shortDescription : (this.shortDescription = paths.basename(labels.getPathLabel(paths.dirname(this.resource.fsPath), void 0, this.environmentService))); + description = this.shortDescription; break; case Verbosity.LONG: - description = this.longDescription ? this.longDescription : (this.longDescription = labels.getPathLabel(paths.dirname(this.resource.fsPath), void 0, this.environmentService)); + description = this.longDescription; break; case Verbosity.MEDIUM: default: - description = this.mediumDescription ? this.mediumDescription : (this.mediumDescription = labels.getPathLabel(paths.dirname(this.resource.fsPath), this.contextService, this.environmentService)); + description = this.mediumDescription; break; } return description; } + @memoize + private get shortTitle(): string { + return this.getName(); + } + + @memoize + private get mediumTitle(): string { + return labels.getPathLabel(this.resource, this.contextService, this.environmentService); + } + + @memoize + private get longTitle(): string { + return labels.getPathLabel(this.resource, void 0, this.environmentService); + } + public getTitle(verbosity: Verbosity): string { let title: string; switch (verbosity) { case Verbosity.SHORT: - title = this.shortTitle ? this.shortTitle : (this.shortTitle = this.getName()); + title = this.shortTitle; break; case Verbosity.MEDIUM: - title = this.mediumTitle ? this.mediumTitle : (this.mediumTitle = labels.getPathLabel(this.resource, this.contextService, this.environmentService)); + title = this.mediumTitle; break; case Verbosity.LONG: - title = this.longTitle ? this.longTitle : (this.longTitle = labels.getPathLabel(this.resource, void 0, this.environmentService)); + title = this.longTitle; break; } diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorTracker.ts b/src/vs/workbench/parts/files/common/editors/fileEditorTracker.ts index 52124b9fb7e..419a6b49c96 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorTracker.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorTracker.ts @@ -24,6 +24,8 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isLinux } from 'vs/base/common/platform'; import { ResourceQueue } from 'vs/base/common/async'; +import { ResourceMap } from 'vs/base/common/map'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; export class FileEditorTracker implements IWorkbenchContribution { @@ -32,6 +34,7 @@ export class FileEditorTracker implements IWorkbenchContribution { private stacks: IEditorStacksModel; private toUnbind: IDisposable[]; private modelLoadQueue: ResourceQueue; + private activeOutOfWorkspaceWatchers: ResourceMap; constructor( @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @@ -40,11 +43,13 @@ export class FileEditorTracker implements IWorkbenchContribution { @IEditorGroupService private editorGroupService: IEditorGroupService, @IFileService private fileService: IFileService, @IEnvironmentService private environmentService: IEnvironmentService, - @IConfigurationService private configurationService: IConfigurationService + @IConfigurationService private configurationService: IConfigurationService, + @IWorkspaceContextService private contextService: IWorkspaceContextService, ) { this.toUnbind = []; this.stacks = editorGroupService.getStacksModel(); this.modelLoadQueue = new ResourceQueue(); + this.activeOutOfWorkspaceWatchers = new ResourceMap(); this.onConfigurationUpdated(configurationService.getConfiguration()); @@ -63,6 +68,9 @@ export class FileEditorTracker implements IWorkbenchContribution { // Update editors from disk changes this.toUnbind.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); + // Editor changing + this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged())); + // Lifecycle this.lifecycleService.onShutdown(this.dispose, this); @@ -205,8 +213,8 @@ export class FileEditorTracker implements IWorkbenchContribution { if (oldResource.toString() === resource.toString()) { reopenFileResource = newResource; // file got moved } else { - const index = indexOf(resource.fsPath, oldResource.fsPath, !isLinux /* ignorecase */); - reopenFileResource = URI.file(paths.join(newResource.fsPath, resource.fsPath.substr(index + oldResource.fsPath.length + 1))); // parent folder got moved + const index = indexOf(resource.path, oldResource.path, !isLinux /* ignorecase */); + reopenFileResource = newResource.with({ path: paths.join(newResource.path, resource.path.substr(index + oldResource.path.length + 1)) }); // parent folder got moved } // Reopen @@ -290,7 +298,42 @@ export class FileEditorTracker implements IWorkbenchContribution { } } + private onEditorsChanged(): void { + this.handleOutOfWorkspaceWatchers(); + } + + private handleOutOfWorkspaceWatchers(): void { + const visibleOutOfWorkspacePaths = new ResourceMap(); + this.editorService.getVisibleEditors().map(editor => { + return toResource(editor.input, { supportSideBySide: true, filter: 'file' }); + }).filter(fileResource => { + return !!fileResource && !this.contextService.isInsideWorkspace(fileResource); + }).forEach(resource => { + visibleOutOfWorkspacePaths.set(resource, resource); + }); + + // Handle no longer visible out of workspace resources + this.activeOutOfWorkspaceWatchers.forEach(resource => { + if (!visibleOutOfWorkspacePaths.get(resource)) { + this.fileService.unwatchFileChanges(resource); + this.activeOutOfWorkspaceWatchers.delete(resource); + } + }); + + // Handle newly visible out of workspace resources + visibleOutOfWorkspacePaths.forEach(resource => { + if (!this.activeOutOfWorkspaceWatchers.get(resource)) { + this.fileService.watchFileChanges(resource); + this.activeOutOfWorkspaceWatchers.set(resource, resource); + } + }); + } + public dispose(): void { this.toUnbind = dispose(this.toUnbind); + + // Dispose watchers if any + this.activeOutOfWorkspaceWatchers.forEach(resource => this.fileService.unwatchFileChanges(resource)); + this.activeOutOfWorkspaceWatchers.clear(); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/files/common/explorerModel.ts b/src/vs/workbench/parts/files/common/explorerModel.ts index c29cd245f3d..0dfa64420eb 100644 --- a/src/vs/workbench/parts/files/common/explorerModel.ts +++ b/src/vs/workbench/parts/files/common/explorerModel.ts @@ -273,7 +273,8 @@ export class FileStat implements IFileStat { } private updateResource(recursive: boolean): void { - this.resource = URI.file(paths.join(this.parent.resource.fsPath, this.name)); + this.resource = this.parent.resource.with({ path: paths.join(this.parent.resource.path, this.name) }); + // this.resource = URI.file(paths.join(this.parent.resource.fsPath, this.name)); if (recursive) { if (this.isDirectory && this.hasChildren && this.children) { @@ -434,4 +435,4 @@ export class OpenEditor { public getResource(): URI { return toResource(this.editor, { supportSideBySide: true, filter: ['file', 'untitled'] }); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/parts/html/browser/html.contribution.ts b/src/vs/workbench/parts/html/browser/html.contribution.ts index 0362b085b92..0e1fa1374bb 100644 --- a/src/vs/workbench/parts/html/browser/html.contribution.ts +++ b/src/vs/workbench/parts/html/browser/html.contribution.ts @@ -27,7 +27,7 @@ function getActivePreviewsForResource(accessor: ServicesAccessor, resource: URI return accessor.get(IWorkbenchEditorService).getVisibleEditors() .filter(c => c instanceof HtmlPreviewPart && c.model) .map(e => e as HtmlPreviewPart) - .filter(e => e.model.uri.scheme === uri.scheme && e.model.uri.fsPath === uri.fsPath); + .filter(e => e.model.uri.scheme === uri.scheme && e.model.uri.toString() === uri.toString()); } // --- Register Editor diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index c80df1ab899..4df30ec8542 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -791,7 +791,7 @@ abstract class AbstractSettingsEditorContribution extends Disposable { private _hasAssociatedPreferencesModelChanged(associatedPreferencesModelUri: URI): TPromise { return this.preferencesRendererCreationPromise.then(preferencesRenderer => { - return !(preferencesRenderer && preferencesRenderer.associatedPreferencesModel && preferencesRenderer.associatedPreferencesModel.uri.fsPath === associatedPreferencesModelUri.fsPath); + return !(preferencesRenderer && preferencesRenderer.associatedPreferencesModel && preferencesRenderer.associatedPreferencesModel.uri.toString() === associatedPreferencesModelUri.toString()); }); } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesService.ts b/src/vs/workbench/parts/preferences/browser/preferencesService.ts index 49c69f7201f..05fe0487f3e 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesService.ts @@ -122,7 +122,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic resolveContent(uri: URI): TPromise { const workspaceSettingsUri = this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE); - if (workspaceSettingsUri && workspaceSettingsUri.fsPath === uri.fsPath) { + if (workspaceSettingsUri && workspaceSettingsUri.toString() === uri.toString()) { return this.resolveSettingsContentFromWorkspaceConfiguration(); } return this.createPreferencesEditorModel(uri) @@ -135,7 +135,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return promise; } - if (this.defaultSettingsResource.fsPath === uri.fsPath) { + if (this.defaultSettingsResource.toString() === uri.toString()) { promise = TPromise.join([this.extensionService.onReady(), this.fetchMostCommonlyUsedSettings()]) .then(result => { const mostCommonSettings = result[1]; @@ -146,7 +146,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic return promise; } - if (this.defaultResourceSettingsResource.fsPath === uri.fsPath) { + if (this.defaultResourceSettingsResource.toString() === uri.toString()) { promise = TPromise.join([this.extensionService.onReady(), this.fetchMostCommonlyUsedSettings()]) .then(result => { const mostCommonSettings = result[1]; @@ -157,25 +157,25 @@ export class PreferencesService extends Disposable implements IPreferencesServic return promise; } - if (this.defaultKeybindingsResource.fsPath === uri.fsPath) { + if (this.defaultKeybindingsResource.toString() === uri.toString()) { const model = this.instantiationService.createInstance(DefaultKeybindingsEditorModel, uri); promise = TPromise.wrap(model); this.defaultPreferencesEditorModels.set(uri, promise); return promise; } - if (this.workspaceConfigSettingsResource.fsPath === uri.fsPath) { + if (this.workspaceConfigSettingsResource.toString() === uri.toString()) { promise = this.createEditableSettingsEditorModel(ConfigurationTarget.WORKSPACE, uri); this.defaultPreferencesEditorModels.set(uri, promise); return promise; } - if (this.getEditableSettingsURI(ConfigurationTarget.USER).fsPath === uri.fsPath) { + if (this.getEditableSettingsURI(ConfigurationTarget.USER).toString() === uri.toString()) { return this.createEditableSettingsEditorModel(ConfigurationTarget.USER, uri); } const workspaceSettingsUri = this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE); - if (workspaceSettingsUri && workspaceSettingsUri.fsPath === uri.fsPath) { + if (workspaceSettingsUri && workspaceSettingsUri.toString() === uri.toString()) { return this.createEditableSettingsEditorModel(ConfigurationTarget.WORKSPACE, workspaceSettingsUri); } @@ -288,7 +288,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic private createEditableSettingsEditorModel(configurationTarget: ConfigurationTarget, resource: URI): TPromise { const settingsUri = this.getEditableSettingsURI(configurationTarget, resource); if (settingsUri) { - if (settingsUri.fsPath === this.workspaceConfigSettingsResource.fsPath) { + if (settingsUri.toString() === this.workspaceConfigSettingsResource.toString()) { return TPromise.join([this.textModelResolverService.createModelReference(settingsUri), this.textModelResolverService.createModelReference(this.contextService.getWorkspace().configuration)]) .then(([reference, workspaceConfigReference]) => this.instantiationService.createInstance(WorkspaceConfigModel, reference, workspaceConfigReference, configurationTarget, this._onDispose.event)); } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts index 2e69c1dfc87..6decbcc4dd9 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts @@ -332,7 +332,7 @@ export class SettingsTargetsWidget extends Widget { actions.push({ id: 'userSettingsTarget', label: getSettingsTargetName(ConfigurationTarget.USER, userSettingsResource, this.workspaceContextService), - checked: this.uri.fsPath === userSettingsResource.fsPath, + checked: this.uri.toString() === userSettingsResource.toString(), enabled: true, run: () => this.onTargetClicked(userSettingsResource) }); @@ -342,7 +342,7 @@ export class SettingsTargetsWidget extends Widget { actions.push({ id: 'workspaceSettingsTarget', label: getSettingsTargetName(ConfigurationTarget.WORKSPACE, workspaceSettingsResource, this.workspaceContextService), - checked: this.uri.fsPath === workspaceSettingsResource.fsPath, + checked: this.uri.toString() === workspaceSettingsResource.toString(), enabled: true, run: () => this.onTargetClicked(workspaceSettingsResource) }); @@ -355,7 +355,7 @@ export class SettingsTargetsWidget extends Widget { return { id: 'folderSettingsTarget' + index, label: getSettingsTargetName(ConfigurationTarget.FOLDER, folder.uri, this.workspaceContextService), - checked: this.uri.fsPath === folder.uri.fsPath, + checked: this.uri.toString() === folder.uri.toString(), enabled: true, run: () => this.onTargetClicked(folder.uri) }; @@ -366,7 +366,7 @@ export class SettingsTargetsWidget extends Widget { } private onTargetClicked(target: URI): void { - if (this.uri.fsPath === target.fsPath) { + if (this.uri.toString() === target.toString()) { return; } this._onDidTargetChange.fire(target); diff --git a/src/vs/workbench/parts/search/browser/search.contribution.ts b/src/vs/workbench/parts/search/browser/search.contribution.ts index 76b10d2b040..ba0b901980a 100644 --- a/src/vs/workbench/parts/search/browser/search.contribution.ts +++ b/src/vs/workbench/parts/search/browser/search.contribution.ts @@ -190,7 +190,7 @@ class ExplorerViewerActionContributor extends ActionBarContributor { return false; } - return fileResource.isDirectory; + return fileResource.isDirectory && fileResource.resource.scheme === 'file'; } public getSecondaryActions(context: any): IAction[] { diff --git a/src/vs/workbench/parts/search/browser/searchActions.ts b/src/vs/workbench/parts/search/browser/searchActions.ts index 3b328409d7d..3c684f0cda2 100644 --- a/src/vs/workbench/parts/search/browser/searchActions.ts +++ b/src/vs/workbench/parts/search/browser/searchActions.ts @@ -7,6 +7,7 @@ import nls = require('vs/nls'); import DOM = require('vs/base/browser/dom'); import errors = require('vs/base/common/errors'); import paths = require('vs/base/common/paths'); +import resources = require('vs/base/common/resources'); import { TPromise } from 'vs/base/common/winjs.base'; import URI from 'vs/base/common/uri'; import { Action } from 'vs/base/common/actions'; @@ -376,7 +377,7 @@ export const findInFolderCommand = (accessor: ServicesAccessor, resource?: URI) if (focused) { const file = explorerItemToFileResource(focused); if (file) { - resource = file.isDirectory ? file.resource : URI.file(paths.dirname(file.resource.fsPath)); + resource = file.isDirectory ? file.resource : resources.dirname(file.resource); } } } diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/node/configuration.ts index fe168920f34..a5199b8236c 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -854,7 +854,7 @@ export class Configuration extends BaseConfiguration { } deleteFolderConfiguration(folder: URI): boolean { - if (this._workspace && this._workspace.folders.length > 0 && this._workspace.folders[0].uri.fsPath === folder.fsPath) { + if (this._workspace && this._workspace.folders.length > 0 && this._workspace.folders[0].uri.toString() === folder.toString()) { // Do not remove workspace configuration return false; } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index f2ac5bbf146..52fa698bfd4 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -23,6 +23,7 @@ import { getPathLabel } from 'vs/base/common/labels'; import { ResourceMap } from 'vs/base/common/map'; import { once } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; export interface IEditorPart { openEditor(input?: IEditorInput, options?: IEditorOptions | ITextEditorOptions, sideBySide?: boolean): TPromise; @@ -53,7 +54,8 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { @IUntitledEditorService private untitledEditorService: IUntitledEditorService, @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, @IInstantiationService private instantiationService: IInstantiationService, - @IEnvironmentService private environmentService: IEnvironmentService + @IEnvironmentService private environmentService: IEnvironmentService, + @IFileService private fileService: IFileService ) { this.editorPart = editorPart; this.fileInputFactory = Registry.as(Extensions.Editors).getFileInputFactory(); @@ -273,7 +275,7 @@ export class WorkbenchEditorService implements IWorkbenchEditorService { } let input: ICachedEditorInput; - if (resource.scheme === network.Schemas.file) { + if (resource.scheme === network.Schemas.file || this.fileService.supportResource && this.fileService.supportResource(resource)) { input = this.fileInputFactory.createFileInput(resource, encoding, instantiationService); } else { input = instantiationService.createInstance(ResourceEditorInput, label, description, resource); @@ -319,14 +321,16 @@ export class DelegatingWorkbenchEditorService extends WorkbenchEditorService { @IInstantiationService instantiationService: IInstantiationService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, - @IEnvironmentService environmentService: IEnvironmentService + @IEnvironmentService environmentService: IEnvironmentService, + @IFileService fileService: IFileService ) { super( editorService, untitledEditorService, workspaceContextService, instantiationService, - environmentService + environmentService, + fileService ); } @@ -359,4 +363,4 @@ export class DelegatingWorkbenchEditorService extends WorkbenchEditorService { return super.doCloseEditor(position, input); }); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 23644861e3e..d2c61424a26 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -11,17 +11,13 @@ import paths = require('vs/base/common/paths'); import encoding = require('vs/base/node/encoding'); import errors = require('vs/base/common/errors'); import uri from 'vs/base/common/uri'; -import { toResource } from 'vs/workbench/common/editor'; import { FileOperation, FileOperationEvent, IFileService, IFilesConfiguration, IResolveFileOptions, IFileStat, IResolveFileResult, IContent, IStreamContent, IImportResult, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, ICreateFileOptions } from 'vs/platform/files/common/files'; import { FileService as NodeFileService, IFileServiceOptions, IEncodingOverride } from 'vs/workbench/services/files/node/fileService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Action } from 'vs/base/common/actions'; -import { ResourceMap } from 'vs/base/common/map'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IMessageService, IMessageWithAction, Severity, CloseAction } from 'vs/platform/message/common/message'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import Event, { Emitter } from 'vs/base/common/event'; @@ -40,24 +36,20 @@ export class FileService implements IFileService { private raw: IFileService; private toUnbind: IDisposable[]; - private activeOutOfWorkspaceWatchers: ResourceMap; protected _onFileChanges: Emitter; - private _onAfterOperation: Emitter; + protected _onAfterOperation: Emitter; constructor( @IConfigurationService private configurationService: IConfigurationService, @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IEnvironmentService private environmentService: IEnvironmentService, - @IEditorGroupService private editorGroupService: IEditorGroupService, @ILifecycleService private lifecycleService: ILifecycleService, @IMessageService private messageService: IMessageService, @IStorageService private storageService: IStorageService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService ) { this.toUnbind = []; - this.activeOutOfWorkspaceWatchers = new ResourceMap(); this._onFileChanges = new Emitter(); this.toUnbind.push(this._onFileChanges); @@ -129,9 +121,6 @@ export class FileService implements IFileService { // Config changes this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationChange(this.configurationService.getConfiguration()))); - // Editor changing - this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged())); - // Root changes this.toUnbind.push(this.contextService.onDidChangeWorkspaceFolders(() => this.onDidChangeWorkspaceFolders())); @@ -153,37 +142,6 @@ export class FileService implements IFileService { return encodingOverride; } - private onEditorsChanged(): void { - this.handleOutOfWorkspaceWatchers(); - } - - private handleOutOfWorkspaceWatchers(): void { - const visibleOutOfWorkspacePaths = new ResourceMap(); - this.editorService.getVisibleEditors().map(editor => { - return toResource(editor.input, { supportSideBySide: true, filter: 'file' }); - }).filter(fileResource => { - return !!fileResource && !this.contextService.isInsideWorkspace(fileResource); - }).forEach(resource => { - visibleOutOfWorkspacePaths.set(resource, resource); - }); - - // Handle no longer visible out of workspace resources - this.activeOutOfWorkspaceWatchers.forEach(resource => { - if (!visibleOutOfWorkspacePaths.get(resource)) { - this.unwatchFileChanges(resource); - this.activeOutOfWorkspaceWatchers.delete(resource); - } - }); - - // Handle newly visible out of workspace resources - visibleOutOfWorkspacePaths.forEach(resource => { - if (!this.activeOutOfWorkspaceWatchers.get(resource)) { - this.watchFileChanges(resource); - this.activeOutOfWorkspaceWatchers.set(resource, resource); - } - }); - } - private onConfigurationChange(configuration: IFilesConfiguration): void { this.updateOptions(configuration.files); } @@ -297,10 +255,6 @@ export class FileService implements IFileService { public dispose(): void { this.toUnbind = dispose(this.toUnbind); - // Dispose watchers if any - this.activeOutOfWorkspaceWatchers.forEach(resource => this.unwatchFileChanges(resource)); - this.activeOutOfWorkspaceWatchers.clear(); - // Dispose service this.raw.dispose(); } diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts index 990dfba275b..d4746609149 100644 --- a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -6,32 +6,114 @@ import URI from 'vs/base/common/uri'; import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; -import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; +import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, IResolveFileOptions, IResolveFileResult, FileOperationEvent, FileOperation, IFileSystemProvider, IStat, FileType, IImportResult, FileChangesEvent } from 'vs/platform/files/common/files'; import { TPromise } from 'vs/base/common/winjs.base'; -import Event from 'vs/base/common/event'; -import { EventEmitter } from 'events'; -import { basename } from 'path'; +import { basename, join } from 'path'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { groupBy, isFalsyOrEmpty, distinct } from 'vs/base/common/arrays'; +import { compare } from 'vs/base/common/strings'; +import { Schemas } from 'vs/base/common/network'; +import { Progress } from 'vs/platform/progress/common/progress'; +import { decodeStream, encode } from 'vs/base/node/encoding'; +import { TrieMap } from 'vs/base/common/map'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; +import { IMessageService } from 'vs/platform/message/common/message'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; +import { IExtensionService } from 'vs/platform/extensions/common/extensions'; -export interface IRemoteFileSystemProvider { - onDidChange: Event; - resolve(resource: URI): TPromise; - update(resource: URI, content: string): TPromise; +function toIFileStat(provider: IFileSystemProvider, stat: IStat, recurse?: (stat: IStat) => boolean): TPromise { + const ret: IFileStat = { + isDirectory: false, + hasChildren: false, + resource: stat.resource, + name: basename(stat.resource.path), + mtime: stat.mtime, + size: stat.size, + etag: stat.mtime.toString(29) + stat.size.toString(31), + }; + + if (stat.type === FileType.File) { + // done + return TPromise.as(ret); + + } else { + // dir -> resolve + return provider.readdir(stat.resource).then(items => { + ret.isDirectory = true; + ret.hasChildren = items.length > 0; + + if (recurse && recurse(stat)) { + // resolve children if requested + return TPromise.join(items.map(stat => toIFileStat(provider, stat, recurse))).then(children => { + ret.children = children; + return ret; + }); + } else { + return ret; + } + }); + } +} + +export function toDeepIFileStat(provider: IFileSystemProvider, stat: IStat, to: URI[]): TPromise { + + const trie = new TrieMap(); + trie.insert(stat.resource.toString(), true); + + if (!isFalsyOrEmpty(to)) { + to.forEach(uri => trie.insert(uri.toString(), true)); + } + + return toIFileStat(provider, stat, candidate => { + const sub = trie.findSuperstr(candidate.resource.toString()); + return !!sub; + }); } export class RemoteFileService extends FileService { - private readonly _provider = new Map(); + private readonly _provider = new Map(); + private _supportedSchemes: string[]; - registerProvider(authority: string, provider: IRemoteFileSystemProvider): IDisposable { + constructor( + @IExtensionService private readonly _extensionService: IExtensionService, + @IStorageService private readonly _storageService: IStorageService, + @IConfigurationService configurationService: IConfigurationService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IEnvironmentService environmentService: IEnvironmentService, + @ILifecycleService lifecycleService: ILifecycleService, + @IMessageService messageService: IMessageService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, + ) { + super( + configurationService, + contextService, + environmentService, + lifecycleService, + messageService, + _storageService, + textResourceConfigurationService, + ); + + this._supportedSchemes = JSON.parse(this._storageService.get('remote_schemes', undefined, '[]')); + } + + registerProvider(authority: string, provider: IFileSystemProvider): IDisposable { if (this._provider.has(authority)) { throw new Error(); } + this._supportedSchemes.push(authority); + this._storageService.store('remote_schemes', JSON.stringify(distinct(this._supportedSchemes))); + this._provider.set(authority, provider); - const reg = provider.onDidChange(e => { + const reg = provider.onDidChange(changes => { // forward change events - this._onFileChanges.fire(new FileChangesEvent([{ resource: e, type: FileChangeType.UPDATED }])); + this._onFileChanges.fire(new FileChangesEvent(changes)); }); return { dispose: () => { @@ -41,70 +123,306 @@ export class RemoteFileService extends FileService { }; } + supportResource(resource: URI): boolean { + return resource.scheme === Schemas.file + || this._provider.has(resource.scheme) + // TODO@remote + || this._supportedSchemes.indexOf(resource.scheme) >= 0; + } + + // --- stat + + private _withProvider(resource: URI): TPromise { + return this._extensionService.activateByEvent('onFileSystemAccess:' + resource.scheme).then(() => { + const provider = this._provider.get(resource.scheme); + if (!provider) { + throw new Error('ENOPRO - no provider known for ' + resource); + } + return provider; + }); + } + + async existsFile(resource: URI): TPromise { + if (resource.scheme === Schemas.file) { + return super.existsFile(resource); + } else { + const provider = await this._withProvider(resource); + return provider + ? this._doResolveFiles(provider, [{ resource }]).then(data => data.length > 0) + : true; + } + } + + async resolveFile(resource: URI, options?: IResolveFileOptions): TPromise { + if (resource.scheme === Schemas.file) { + return super.resolveFile(resource, options); + } else { + const provider = await this._withProvider(resource); + if (!provider) { + throw new Error('ENOENT'); + } + return this._doResolveFiles(provider, [{ resource, options }]).then(data => { + if (isFalsyOrEmpty(data)) { + throw new Error('NotFound'); + } + return data[0].stat; + }); + } + } + + async resolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): TPromise { + const groups = groupBy(toResolve, (a, b) => compare(a.resource.scheme, b.resource.scheme)); + const promises: TPromise[] = []; + for (const group of groups) { + if (group[0].resource.scheme === Schemas.file) { + promises.push(super.resolveFiles(group)); + } else { + const provider = await this._withProvider(group[0].resource); + if (provider) { + promises.push(this._doResolveFiles(provider, group)); + } + } + } + return TPromise.join(promises).then(data => { + return [].concat(...data); + }); + } + + private _doResolveFiles(provider: IFileSystemProvider, toResolve: { resource: URI; options?: IResolveFileOptions; }[]): TPromise { + let result: IResolveFileResult[] = []; + let promises: TPromise[] = []; + for (const item of toResolve) { + promises.push(provider.stat(item.resource) + .then(stat => toDeepIFileStat(provider, stat, item.options && item.options.resolveTo)) + .then(stat => result.push({ stat, success: true }))); + } + return TPromise.join(promises).then(() => result); + } + // --- resolve - resolveContent(resource: URI, options?: IResolveContentOptions): TPromise { - if (this._provider.has(resource.authority)) { - return this._doResolveContent(resource); + async resolveContent(resource: URI, options?: IResolveContentOptions): TPromise { + if (resource.scheme === Schemas.file) { + return super.resolveContent(resource, options); + } else { + const provider = await this._withProvider(resource); + return this._doResolveContent(provider, resource).then(RemoteFileService._asContent); } - - return super.resolveContent(resource, options); } - resolveStreamContent(resource: URI, options?: IResolveContentOptions): TPromise { - if (this._provider.has(resource.authority)) { - return this._doResolveContent(resource).then(RemoteFileService._asStreamContent); + async resolveStreamContent(resource: URI, options?: IResolveContentOptions): TPromise { + if (resource.scheme === Schemas.file) { + return super.resolveStreamContent(resource, options); + } else { + const provider = await this._withProvider(resource); + return this._doResolveContent(provider, resource); } - - return super.resolveStreamContent(resource, options); } - private async _doResolveContent(resource: URI): TPromise { + private async _doResolveContent(provider: IFileSystemProvider, resource: URI): TPromise { - const stat = RemoteFileService._createFakeStat(resource); - const value = await this._provider.get(resource.authority).resolve(resource); - return { ...stat, value }; + const stat = await toIFileStat(provider, await provider.stat(resource)); + + const encoding = this.getEncoding(resource); + const stream = decodeStream(encoding); + await provider.read(resource, new Progress(chunk => stream.write(chunk))); + stream.end(); + + return { + encoding, + value: stream, + resource: stat.resource, + name: stat.name, + etag: stat.etag, + mtime: stat.mtime, + }; } // --- saving - updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise { - if (this._provider.has(resource.authority)) { - return this._doUpdateContent(resource, value).then(RemoteFileService._createFakeStat); + async createFile(resource: URI, content?: string): TPromise { + if (resource.scheme === Schemas.file) { + return super.createFile(resource, content); + } else { + const provider = await this._withProvider(resource); + const stat = await this._doUpdateContent(provider, resource, content || '', {}); + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, stat)); + return stat; + } + } + + async updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise { + if (resource.scheme === Schemas.file) { + return super.updateContent(resource, value, options); + } else { + const provider = await this._withProvider(resource); + return this._doUpdateContent(provider, resource, value, options || {}); + } + } + + private async _doUpdateContent(provider: IFileSystemProvider, resource: URI, content: string, options: IUpdateContentOptions): TPromise { + const encoding = this.getEncoding(resource, options.encoding); + await provider.write(resource, encode(content, encoding)); + const stat = await provider.stat(resource); + const fileStat = await toIFileStat(provider, stat); + return fileStat; + } + + private static _asContent(content: IStreamContent): TPromise { + return new TPromise((resolve, reject) => { + let result: IContent = { + value: '', + encoding: content.encoding, + etag: content.etag, + mtime: content.mtime, + name: content.name, + resource: content.resource + }; + content.value.on('data', chunk => result.value += chunk); + content.value.on('error', reject); + content.value.on('end', () => resolve(result)); + }); + } + + // --- delete + + async del(resource: URI, useTrash?: boolean): TPromise { + if (resource.scheme === Schemas.file) { + return super.del(resource, useTrash); + } else { + const provider = await this._withProvider(resource); + const stat = await provider.stat(resource); + await stat.type === FileType.Dir ? provider.rmdir(resource) : provider.unlink(resource); + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); + } + } + + async createFolder(resource: URI): TPromise { + if (resource.scheme === Schemas.file) { + return super.createFolder(resource); + } else { + const provider = await this._withProvider(resource); + await provider.mkdir(resource); + const stat = await toIFileStat(provider, await provider.stat(resource)); + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, stat)); + return stat; + } + } + + async rename(resource: URI, newName: string): TPromise { + if (resource.scheme === Schemas.file) { + return super.rename(resource, newName); + } else { + const provider = await this._withProvider(resource); + const target = resource.with({ path: join(resource.path, '..', newName) }); + return this._doMove(provider, resource, target, false); + } + } + + async moveFile(source: URI, target: URI, overwrite?: boolean): TPromise { + if (source.scheme !== target.scheme) { + return this._manualMove(source, target); + } else if (source.scheme === Schemas.file) { + return super.moveFile(source, target, overwrite); + } else { + const provider = await this._withProvider(source); + return this._doMove(provider, source, target, overwrite); + } + } + + private async _doMove(provider: IFileSystemProvider, source: URI, target: URI, overwrite?: boolean): TPromise { + if (overwrite) { + try { + await this.del(target); + } catch (e) { + // TODO@Joh Better errors + // ignore not_exists error + // abort on other errors + } + } + await provider.rename(source, target); + const stat = await this.resolveFile(target); + this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.MOVE, stat)); + return stat; + } + + private async _manualMove(source: URI, target: URI, overwrite?: boolean): TPromise { + await this.copyFile(source, target, overwrite); + await this.del(source); + const stat = await this.resolveFile(target); + this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.MOVE, stat)); + return stat; + } + + importFile(source: URI, targetFolder: URI): TPromise { + if (source.scheme === targetFolder.scheme && source.scheme === Schemas.file) { + return super.importFile(source, targetFolder); + } else { + const target = targetFolder.with({ path: join(targetFolder.path, basename(source.path)) }); + return this.copyFile(source, target, false).then(stat => ({ stat, isNew: false })); + } + } + + async copyFile(source: URI, target: URI, overwrite?: boolean): TPromise { + if (source.scheme === target.scheme && source.scheme === Schemas.file) { + return super.copyFile(source, target, overwrite); } - return super.updateContent(resource, value, options); + if (overwrite) { + try { + await this.del(target); + } catch (e) { + // TODO@Joh Better errors + // ignore not_exists error + // abort on other errors + } + } + // TODO@Joh This does only work for textfiles + // because the content turns things into a string + // and all binary data will be broken + const content = await this.resolveContent(source); + const targetProvider = await this._withProvider(target); + + if (targetProvider) { + const stat = await this._doUpdateContent(targetProvider, target, content.value, { encoding: content.encoding }); + this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.COPY, stat)); + return stat; + } else { + return super.updateContent(target, content.value, { encoding: content.encoding }); + } } - private async _doUpdateContent(resource: URI, content: string): TPromise { - await this._provider.get(resource.authority).update(resource, content); - return resource; + async touchFile(resource: URI): TPromise { + if (resource.scheme === Schemas.file) { + return super.touchFile(resource); + } else { + const provider = await this._withProvider(resource); + return this._doTouchFile(provider, resource); + } } - // --- util - - private static _createFakeStat(resource: URI): IFileStat { - - return { - resource, - name: basename(resource.path), - encoding: 'utf8', - mtime: Date.now(), - etag: Date.now().toString(16), - isDirectory: false, - hasChildren: false - }; + private async _doTouchFile(provider: IFileSystemProvider, resource: URI): TPromise { + let stat: IStat; + try { + await provider.stat(resource); + stat = await provider.utimes(resource, Date.now()); + } catch (e) { + // TODO@Joh, if ENOENT + await provider.write(resource, new Uint8Array(0)); + stat = await provider.stat(resource); + } + return toIFileStat(provider, stat); } - private static _asStreamContent(content: IContent): IStreamContent { - const emitter = new EventEmitter(); - const { value } = content; - const result = content; - result.value = emitter; - setTimeout(() => { - emitter.emit('data', value); - emitter.emit('end'); - }, 0); - return result; + // TODO@Joh - file watching on demand! + public watchFileChanges(resource: URI): void { + if (resource.scheme === Schemas.file) { + super.watchFileChanges(resource); + } + } + public unwatchFileChanges(resource: URI): void { + if (resource.scheme === Schemas.file) { + super.unwatchFileChanges(resource); + } } } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 579388facea..80db2087ae8 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -12,7 +12,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { guessMimeTypes } from 'vs/base/common/mime'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import URI from 'vs/base/common/uri'; -import * as assert from 'vs/base/common/assert'; +// import * as assert from 'vs/base/common/assert'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import paths = require('vs/base/common/paths'); import diagnostics = require('vs/base/common/diagnostics'); @@ -90,7 +90,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil ) { super(modelService, modeService); - assert.ok(resource.scheme === 'file', 'TextFileEditorModel can only handle file:// resources.'); + // TODO@remote + // assert.ok(resource.scheme === 'file', 'TextFileEditorModel can only handle file:// resources.'); this.resource = resource; this.toDispose = []; diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 33f5ee1e9d1..e754ef1bf38 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -407,10 +407,14 @@ export abstract class TextFileService implements ITextFileService { const filesToSave: URI[] = []; const untitledToSave: URI[] = []; toSave.forEach(s => { - if (s.scheme === Schemas.file) { - filesToSave.push(s); - } else if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && s.scheme === UNTITLED_SCHEMA) { + // TODO@remote + // if (s.scheme === Schemas.file) { + // filesToSave.push(s); + // } else + if ((Array.isArray(arg1) || arg1 === true /* includeUntitled */) && s.scheme === UNTITLED_SCHEMA) { untitledToSave.push(s); + } else { + filesToSave.push(s); } }); @@ -712,4 +716,4 @@ export abstract class TextFileService implements ITextFileService { // Clear all caches this._models.clear(); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts index c7b7668b869..125ef301221 100644 --- a/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts +++ b/src/vs/workbench/services/textmodelResolver/common/textModelResolverService.ts @@ -35,7 +35,10 @@ class ResourceModelCollection extends ReferenceCollection this.instantiationService.createInstance(ResourceEditorModel, resource)); }