All files / providers user-file-system-provider.ts

92.98% Statements 53/57
84% Branches 21/25
77.77% Functions 14/18
92.72% Lines 51/55

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176          10x                           9x   9x   9x   9x                   13x   9x 12x 11x   2x                 31x               5x   5x       8x   8x 1x     7x 2x   2x                   5x 3x     2x       4x   4x 2x     2x       10x       10x   1x         2x   2x 1x     1x       1x   1x       1x       5x 5x         5x   5x 9x 1x     8x 8x   8x 3x   5x   5x 4x         5x       5x                        
import * as vscode from 'vscode';
import { SettingsFileStore } from '@src/services/core';
import { UserUri } from './user-uri';
import { getErrorMessage } from '@src/utilities/error';
 
const encoder = new TextEncoder();
 
/**
 * Provides the settings-backed `user:///` virtual file system.
 *
 * All files live in a single {@link SettingsFileStore} — the map-typed
 * `mentor.files` settings key in the user scope — keyed by their full,
 * slash-delimited virtual path. The URI path maps one-to-one to the store key:
 * `user:///shapes/my-shapes.ttl` is the entry `shapes/my-shapes.ttl`. Folders
 * are not stored; intermediate directories are synthesized from the key
 * prefixes. Files under this scheme therefore sync across machines via Settings
 * Sync and exist independently of any workspace.
 */
export class UserFileSystemProvider implements vscode.FileSystemProvider {
	private readonly _onDidChangeFile = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
 
	readonly onDidChangeFile = this._onDidChangeFile.event;
 
	constructor(context: vscode.ExtensionContext, private readonly _store: SettingsFileStore) {
		// Self-register with the extension context for automatic disposal.
		context.subscriptions.push(
			vscode.workspace.registerFileSystemProvider(UserUri.uriScheme, this, {
				isCaseSensitive: true,
				isReadonly: false
			})
		);
 
		// Relay store change events as file change events. The store observes the
		// backing settings value, so local writes AND cross-machine Settings Sync
		// updates both surface here and refresh open editors.
		const toUri = (key: string) => vscode.Uri.parse(UserUri.forPath(key));
 
		context.subscriptions.push(this._store.onDidChangeEntries(e => {
			this._onDidChangeFile.fire([
				...e.created.map(key => ({ type: vscode.FileChangeType.Created, uri: toUri(key) })),
				...e.changed.map(key => ({ type: vscode.FileChangeType.Changed, uri: toUri(key) })),
				...e.deleted.map(key => ({ type: vscode.FileChangeType.Deleted, uri: toUri(key) })),
			]);
		}));
	}
 
	/**
	 * Resolves a URI to its store entry key (the path without the leading slash).
	 */
	private _key(uri: vscode.Uri): string {
		return uri.path.replace(/^\/+/, '');
	}
 
	/**
	 * Indicates whether any stored file lives under the given directory key,
	 * i.e. whether the key names a (synthesized) directory.
	 */
	private _isDirectory(key: string): boolean {
		const prefix = `${key}/`;
 
		return this._store.keys().some(k => k.startsWith(prefix));
	}
 
	async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
		const key = this._key(uri);
 
		if (key === '') {
			return { type: vscode.FileType.Directory, ctime: 0, mtime: 0, size: 0 };
		}
 
		if (this._store.has(key)) {
			const content = await this._store.read(key);
 
			return {
				type: vscode.FileType.File,
				ctime: 0,
				// Bumped by the store whenever the backing settings value changes, so
				// editors detect external (e.g. Settings Sync) updates.
				mtime: this._store.mtime(key),
				size: encoder.encode(content).length,
			};
		}
 
		if (this._isDirectory(key)) {
			return { type: vscode.FileType.Directory, ctime: 0, mtime: 0, size: 0 };
		}
 
		throw vscode.FileSystemError.FileNotFound(uri);
	}
 
	async readFile(uri: vscode.Uri): Promise<Uint8Array> {
		const key = this._key(uri);
 
		if (!this._store.has(key)) {
			throw vscode.FileSystemError.FileNotFound(uri);
		}
 
		return encoder.encode(await this._store.read(key));
	}
 
	async writeFile(uri: vscode.Uri, content: Uint8Array): Promise<void> {
		try {
			// The store observes the resulting configuration change and reports it
			// through onDidChangeEntries, which is relayed as the file change event —
			// one event path for local writes and synced updates alike.
			await this._store.write(this._key(uri), new TextDecoder().decode(content));
		} catch (error) {
			throw vscode.FileSystemError.Unavailable(getErrorMessage(error));
		}
	}
 
	async delete(uri: vscode.Uri): Promise<void> {
		const key = this._key(uri);
 
		if (!this._store.has(key)) {
			throw vscode.FileSystemError.FileNotFound(uri);
		}
 
		await this._store.delete(key);
	}
 
	async rename(oldUri: vscode.Uri, newUri: vscode.Uri): Promise<void> {
		const source = this._key(oldUri);
 
		Iif (!this._store.has(source)) {
			throw vscode.FileSystemError.FileNotFound(oldUri);
		}
 
		await this._store.rename(source, this._key(newUri));
	}
 
	readDirectory(uri: vscode.Uri): [string, vscode.FileType][] {
		const key = this._key(uri);
		const prefix = key === '' ? '' : `${key}/`;
 
		// Directories are synthesized from key prefixes: for each stored key under
		// the directory, take the next path segment; a segment that completes a key
		// is a file, otherwise it names a subdirectory.
		const entries = new Map<string, vscode.FileType>();
 
		for (const storeKey of this._store.keys()) {
			if (!storeKey.startsWith(prefix)) {
				continue;
			}
 
			const remainder = storeKey.slice(prefix.length);
			const slash = remainder.indexOf('/');
 
			if (slash === -1) {
				entries.set(remainder, vscode.FileType.File);
			} else {
				const segment = remainder.slice(0, slash);
 
				if (!entries.has(segment)) {
					entries.set(segment, vscode.FileType.Directory);
				}
			}
		}
 
		Iif (entries.size === 0 && key !== '' && !this._isDirectory(key)) {
			throw vscode.FileSystemError.FileNotFound(uri);
		}
 
		return [...entries];
	}
 
	createDirectory(): void {
		// Folders are implicit in the flat store: a directory materializes as soon
		// as a file is written under its path. Nothing to persist.
	}
 
	watch(): vscode.Disposable {
		return new vscode.Disposable(() => { });
	}
}