extHostFileSystem.ts 11.2 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { URI, UriComponents } from 'vs/base/common/uri';
J
Johannes Rieken 已提交
7
import { MainContext, IMainContext, ExtHostFileSystemShape, MainThreadFileSystemShape, IFileChangeDto } from './extHost.protocol';
8
import * as vscode from 'vscode';
9
import * as files from 'vs/platform/files/common/files';
10
import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
J
Johannes Rieken 已提交
11 12 13
import { FileChangeType } from 'vs/workbench/api/common/extHostTypes';
import * as typeConverter from 'vs/workbench/api/common/extHostTypeConverters';
import { ExtHostLanguageFeatures } from 'vs/workbench/api/common/extHostLanguageFeatures';
14
import { Schemas } from 'vs/base/common/network';
15
import { ResourceLabelFormatter } from 'vs/platform/label/common/label';
16
import { State, StateMachine, LinkComputer, Edge } from 'vs/editor/common/modes/linkComputer';
17 18
import { commonPrefixLength } from 'vs/base/common/strings';
import { CharCode } from 'vs/base/common/charCode';
19

20
class FsLinkProvider {
21

22
	private _schemes: string[] = [];
23
	private _stateMachine?: StateMachine;
24 25

	add(scheme: string): void {
26 27
		this._stateMachine = undefined;
		this._schemes.push(scheme);
28 29 30
	}

	delete(scheme: string): void {
31
		const idx = this._schemes.indexOf(scheme);
32 33
		if (idx >= 0) {
			this._schemes.splice(idx, 1);
34
			this._stateMachine = undefined;
35 36 37
		}
	}

38 39 40 41 42 43
	private _initStateMachine(): void {
		if (!this._stateMachine) {

			// sort and compute common prefix with previous scheme
			// then build state transitions based on the data
			const schemes = this._schemes.sort();
44 45
			const edges: Edge[] = [];
			let prevScheme: string | undefined;
46 47 48 49 50 51 52 53 54 55 56
			let prevState: State;
			let nextState = State.LastKnownState;
			for (const scheme of schemes) {

				// skip the common prefix of the prev scheme
				// and continue with its last state
				let pos = !prevScheme ? 0 : commonPrefixLength(prevScheme, scheme);
				if (pos === 0) {
					prevState = State.Start;
				} else {
					prevState = nextState;
57
				}
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96

				for (; pos < scheme.length; pos++) {
					// keep creating new (next) states until the
					// end (and the BeforeColon-state) is reached
					if (pos + 1 === scheme.length) {
						nextState = State.BeforeColon;
					} else {
						nextState += 1;
					}
					edges.push([prevState, scheme.toUpperCase().charCodeAt(pos), nextState]);
					edges.push([prevState, scheme.toLowerCase().charCodeAt(pos), nextState]);
					prevState = nextState;
				}

				prevScheme = scheme;
			}

			// all link must match this pattern `<scheme>:/<more>`
			edges.push([State.BeforeColon, CharCode.Colon, State.AfterColon]);
			edges.push([State.AfterColon, CharCode.Slash, State.End]);

			this._stateMachine = new StateMachine(edges);
		}
	}

	provideDocumentLinks(document: vscode.TextDocument): vscode.ProviderResult<vscode.DocumentLink[]> {
		this._initStateMachine();

		const result: vscode.DocumentLink[] = [];
		const links = LinkComputer.computeLinks({
			getLineContent(lineNumber: number): string {
				return document.lineAt(lineNumber - 1).text;
			},
			getLineCount(): number {
				return document.lineCount;
			}
		}, this._stateMachine);

		for (const link of links) {
97
			const docLink = typeConverter.DocumentLink.to(link);
M
Martin Aeschlimann 已提交
98 99
			if (docLink.target) {
				result.push(docLink);
100 101 102 103 104
			}
		}
		return result;
	}
}
105 106 107 108

export class ExtHostFileSystem implements ExtHostFileSystemShape {

	private readonly _proxy: MainThreadFileSystemShape;
109
	private readonly _linkProvider = new FsLinkProvider();
110
	private readonly _fsProvider = new Map<number, vscode.FileSystemProvider>();
111
	private readonly _usedSchemes = new Set<string>();
112
	private readonly _watches = new Map<number, IDisposable>();
113

114
	private _linkProviderRegistration: IDisposable;
115
	// Used as a handle both for file system providers and resource label formatters (being lazy)
116 117
	private _handlePool: number = 0;

118
	constructor(mainContext: IMainContext, private _extHostLanguageFeatures: ExtHostLanguageFeatures) {
119
		this._proxy = mainContext.getProxy(MainContext.MainThreadFileSystem);
120 121 122 123 124 125 126 127 128
		this._usedSchemes.add(Schemas.file);
		this._usedSchemes.add(Schemas.untitled);
		this._usedSchemes.add(Schemas.vscode);
		this._usedSchemes.add(Schemas.inMemory);
		this._usedSchemes.add(Schemas.internal);
		this._usedSchemes.add(Schemas.http);
		this._usedSchemes.add(Schemas.https);
		this._usedSchemes.add(Schemas.mailto);
		this._usedSchemes.add(Schemas.data);
129
		this._usedSchemes.add(Schemas.command);
130
	}
131

132 133 134 135 136 137 138 139
	dispose(): void {
		dispose(this._linkProviderRegistration);
	}

	private _registerLinkProviderIfNotYetRegistered(): void {
		if (!this._linkProviderRegistration) {
			this._linkProviderRegistration = this._extHostLanguageFeatures.registerDocumentLinkProvider(undefined, '*', this._linkProvider);
		}
140 141
	}

I
isidor 已提交
142
	registerFileSystemProvider(scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean, isReadonly?: boolean } = {}) {
143 144 145 146 147

		if (this._usedSchemes.has(scheme)) {
			throw new Error(`a provider for the scheme '${scheme}' is already registered`);
		}

148 149 150
		//
		this._registerLinkProviderIfNotYetRegistered();

151
		const handle = this._handlePool++;
152
		this._linkProvider.add(scheme);
153
		this._usedSchemes.add(scheme);
154
		this._fsProvider.set(handle, provider);
155 156

		let capabilites = files.FileSystemProviderCapabilities.FileReadWrite;
157 158 159
		if (options.isCaseSensitive) {
			capabilites += files.FileSystemProviderCapabilities.PathCaseSensitive;
		}
I
isidor 已提交
160 161 162
		if (options.isReadonly) {
			capabilites += files.FileSystemProviderCapabilities.Readonly;
		}
163 164 165
		if (typeof provider.copy === 'function') {
			capabilites += files.FileSystemProviderCapabilities.FileFolderCopy;
		}
166 167 168 169 170
		if (typeof provider.open === 'function' && typeof provider.close === 'function'
			&& typeof provider.read === 'function' && typeof provider.write === 'function'
		) {
			capabilites += files.FileSystemProviderCapabilities.FileOpenReadWriteClose;
		}
171 172

		this._proxy.$registerFileSystemProvider(handle, scheme, capabilites);
173 174

		const subscription = provider.onDidChangeFile(event => {
175
			const mapped: IFileChangeDto[] = [];
176
			for (const e of event) {
177
				let { uri: resource, type } = e;
178 179 180 181
				if (resource.scheme !== scheme) {
					// dropping events for wrong scheme
					continue;
				}
182
				let newType: files.FileChangeType | undefined;
183
				switch (type) {
184
					case FileChangeType.Changed:
185 186
						newType = files.FileChangeType.UPDATED;
						break;
187
					case FileChangeType.Created:
188 189
						newType = files.FileChangeType.ADDED;
						break;
190
					case FileChangeType.Deleted:
191 192
						newType = files.FileChangeType.DELETED;
						break;
193 194
					default:
						throw new Error('Unknown FileChangeType');
195
				}
196 197 198
				mapped.push({ resource, type: newType });
			}
			this._proxy.$onFileSystemChange(handle, mapped);
199 200
		});

201 202 203 204 205 206 207
		return toDisposable(() => {
			subscription.dispose();
			this._linkProvider.delete(scheme);
			this._usedSchemes.delete(scheme);
			this._fsProvider.delete(handle);
			this._proxy.$unregisterProvider(handle);
		});
208 209
	}

210 211 212 213 214 215 216
	registerResourceLabelFormatter(formatter: ResourceLabelFormatter): IDisposable {
		const handle = this._handlePool++;
		this._proxy.$registerResourceLabelFormatter(handle, formatter);

		return toDisposable(() => {
			this._proxy.$unregisterResourceLabelFormatter(handle);
		});
A
Tweaks  
Alex Dima 已提交
217 218
	}

J
Johannes Rieken 已提交
219
	private static _asIStat(stat: vscode.FileStat): files.IStat {
220 221
		const { type, ctime, mtime, size } = stat;
		return { type, ctime, mtime, size };
J
Johannes Rieken 已提交
222 223
	}

224
	$stat(handle: number, resource: UriComponents): Promise<files.IStat> {
225
		return Promise.resolve(this.getProvider(handle).stat(URI.revive(resource))).then(ExtHostFileSystem._asIStat);
226
	}
227

228
	$readdir(handle: number, resource: UriComponents): Promise<[string, files.FileType][]> {
229
		return Promise.resolve(this.getProvider(handle).readDirectory(URI.revive(resource)));
230
	}
231

232
	$readFile(handle: number, resource: UriComponents): Promise<Buffer> {
233
		return Promise.resolve(this.getProvider(handle).readFile(URI.revive(resource))).then(data => {
234
			return Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength);
235
		});
236
	}
237

238
	$writeFile(handle: number, resource: UriComponents, content: Buffer, opts: files.FileWriteOptions): Promise<void> {
239
		return Promise.resolve(this.getProvider(handle).writeFile(URI.revive(resource), content, opts));
240
	}
241

242
	$delete(handle: number, resource: UriComponents, opts: files.FileDeleteOptions): Promise<void> {
243
		return Promise.resolve(this.getProvider(handle).delete(URI.revive(resource), opts));
244
	}
245

246
	$rename(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): Promise<void> {
247
		return Promise.resolve(this.getProvider(handle).rename(URI.revive(oldUri), URI.revive(newUri), opts));
248
	}
249

250
	$copy(handle: number, oldUri: UriComponents, newUri: UriComponents, opts: files.FileOverwriteOptions): Promise<void> {
251 252 253 254 255
		const provider = this.getProvider(handle);
		if (!provider.copy) {
			throw new Error('FileSystemProvider does not implement "copy"');
		}
		return Promise.resolve(provider.copy(URI.revive(oldUri), URI.revive(newUri), opts));
256 257
	}

258
	$mkdir(handle: number, resource: UriComponents): Promise<void> {
259
		return Promise.resolve(this.getProvider(handle).createDirectory(URI.revive(resource)));
260
	}
261

262
	$watch(handle: number, session: number, resource: UriComponents, opts: files.IWatchOptions): void {
263
		const subscription = this.getProvider(handle).watch(URI.revive(resource), opts);
264
		this._watches.set(session, subscription);
265
	}
266

J
Johannes Rieken 已提交
267
	$unwatch(_handle: number, session: number): void {
268
		const subscription = this._watches.get(session);
269 270 271 272 273
		if (subscription) {
			subscription.dispose();
			this._watches.delete(session);
		}
	}
274

J
Johannes Rieken 已提交
275
	$open(handle: number, resource: UriComponents, opts: files.FileOpenOptions): Promise<number> {
276 277 278 279 280
		const provider = this.getProvider(handle);
		if (!provider.open) {
			throw new Error('FileSystemProvider does not implement "open"');
		}
		return Promise.resolve(provider.open(URI.revive(resource), opts));
281 282
	}

283
	$close(handle: number, fd: number): Promise<void> {
284 285 286 287 288
		const provider = this.getProvider(handle);
		if (!provider.close) {
			throw new Error('FileSystemProvider does not implement "close"');
		}
		return Promise.resolve(provider.close(fd));
289 290
	}

291
	$read(handle: number, fd: number, pos: number, length: number): Promise<Buffer> {
292 293 294 295
		const provider = this.getProvider(handle);
		if (!provider.read) {
			throw new Error('FileSystemProvider does not implement "read"');
		}
296
		const data = Buffer.allocUnsafe(length);
297
		return Promise.resolve(provider.read(fd, pos, data, 0, length)).then(read => {
298 299
			return data.slice(0, read); // don't send zeros
		});
300 301
	}

302
	$write(handle: number, fd: number, pos: number, data: Buffer): Promise<number> {
303 304 305 306 307
		const provider = this.getProvider(handle);
		if (!provider.write) {
			throw new Error('FileSystemProvider does not implement "write"');
		}
		return Promise.resolve(provider.write(fd, pos, data, 0, data.length));
308 309
	}

310 311 312 313 314 315 316 317 318 319
	private getProvider(handle: number): vscode.FileSystemProvider {
		const provider = this._fsProvider.get(handle);
		if (!provider) {
			const err = new Error();
			err.name = 'ENOPRO';
			err.message = `no provider`;
			throw err;
		}
		return provider;
	}
320
}