mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-10 10:12:34 -05:00
Merge from vscode 2cd495805cf99b31b6926f08ff4348124b2cf73d
This commit is contained in:
committed by
AzureDataStudio
parent
a8a7559229
commit
1388493cc1
@@ -237,7 +237,13 @@ export class AuthenticationService extends Disposable implements IAuthentication
|
||||
group: '2_signInRequests',
|
||||
command: {
|
||||
id: `${extensionId}signIn`,
|
||||
title: nls.localize('signInRequest', "Sign in to use {0} (1)", extensionName)
|
||||
title: nls.localize(
|
||||
{
|
||||
key: 'signInRequest',
|
||||
comment: ['The placeholder {0} will be replaced with an extension name. (1) is to indicate that this menu item contributes to a badge count.']
|
||||
},
|
||||
"Sign in to use {0} (1)",
|
||||
extensionName)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,243 +3,20 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mergeSort } from 'vs/base/common/arrays';
|
||||
import { dispose, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler } from 'vs/editor/browser/services/bulkEditService';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model';
|
||||
import { WorkspaceFileEdit, WorkspaceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack';
|
||||
|
||||
type ValidationResult = { canApply: true } | { canApply: false, reason: URI };
|
||||
|
||||
class ModelEditTask implements IDisposable {
|
||||
|
||||
public readonly model: ITextModel;
|
||||
|
||||
protected _edits: IIdentifiedSingleEditOperation[];
|
||||
private _expectedModelVersionId: number | undefined;
|
||||
protected _newEol: EndOfLineSequence | undefined;
|
||||
|
||||
constructor(private readonly _modelReference: IReference<IResolvedTextEditorModel>) {
|
||||
this.model = this._modelReference.object.textEditorModel;
|
||||
this._edits = [];
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._modelReference.dispose();
|
||||
}
|
||||
|
||||
addEdit(resourceEdit: WorkspaceTextEdit): void {
|
||||
this._expectedModelVersionId = resourceEdit.modelVersionId;
|
||||
const { edit } = resourceEdit;
|
||||
|
||||
if (typeof edit.eol === 'number') {
|
||||
// honor eol-change
|
||||
this._newEol = edit.eol;
|
||||
}
|
||||
if (!edit.range && !edit.text) {
|
||||
// lacks both a range and the text
|
||||
return;
|
||||
}
|
||||
if (Range.isEmpty(edit.range) && !edit.text) {
|
||||
// no-op edit (replace empty range with empty text)
|
||||
return;
|
||||
}
|
||||
|
||||
// create edit operation
|
||||
let range: Range;
|
||||
if (!edit.range) {
|
||||
range = this.model.getFullModelRange();
|
||||
} else {
|
||||
range = Range.lift(edit.range);
|
||||
}
|
||||
this._edits.push(EditOperation.replaceMove(range, edit.text));
|
||||
}
|
||||
|
||||
validate(): ValidationResult {
|
||||
if (typeof this._expectedModelVersionId === 'undefined' || this.model.getVersionId() === this._expectedModelVersionId) {
|
||||
return { canApply: true };
|
||||
}
|
||||
return { canApply: false, reason: this.model.uri };
|
||||
}
|
||||
|
||||
getBeforeCursorState(): Selection[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
apply(): void {
|
||||
if (this._edits.length > 0) {
|
||||
this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range));
|
||||
this.model.pushEditOperations(null, this._edits, () => null);
|
||||
}
|
||||
if (this._newEol !== undefined) {
|
||||
this.model.pushEOL(this._newEol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EditorEditTask extends ModelEditTask {
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
|
||||
constructor(modelReference: IReference<IResolvedTextEditorModel>, editor: ICodeEditor) {
|
||||
super(modelReference);
|
||||
this._editor = editor;
|
||||
}
|
||||
|
||||
getBeforeCursorState(): Selection[] | null {
|
||||
return this._editor.getSelections();
|
||||
}
|
||||
|
||||
apply(): void {
|
||||
if (this._edits.length > 0) {
|
||||
this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range));
|
||||
this._editor.executeEdits('', this._edits);
|
||||
}
|
||||
if (this._newEol !== undefined) {
|
||||
if (this._editor.hasModel()) {
|
||||
this._editor.getModel().pushEOL(this._newEol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BulkEditModel implements IDisposable {
|
||||
|
||||
private _edits = new Map<string, WorkspaceTextEdit[]>();
|
||||
private _tasks: ModelEditTask[] | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _label: string | undefined,
|
||||
private readonly _editor: ICodeEditor | undefined,
|
||||
private readonly _progress: IProgress<void>,
|
||||
edits: WorkspaceTextEdit[],
|
||||
@IEditorWorkerService private readonly _editorWorker: IEditorWorkerService,
|
||||
@ITextModelService private readonly _textModelResolverService: ITextModelService,
|
||||
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService
|
||||
) {
|
||||
edits.forEach(this._addEdit, this);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this._tasks) {
|
||||
dispose(this._tasks);
|
||||
}
|
||||
}
|
||||
|
||||
private _addEdit(edit: WorkspaceTextEdit): void {
|
||||
let array = this._edits.get(edit.resource.toString());
|
||||
if (!array) {
|
||||
array = [];
|
||||
this._edits.set(edit.resource.toString(), array);
|
||||
}
|
||||
array.push(edit);
|
||||
}
|
||||
|
||||
async prepare(): Promise<BulkEditModel> {
|
||||
|
||||
if (this._tasks) {
|
||||
throw new Error('illegal state - already prepared');
|
||||
}
|
||||
|
||||
this._tasks = [];
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
for (let [key, value] of this._edits) {
|
||||
const promise = this._textModelResolverService.createModelReference(URI.parse(key)).then(async ref => {
|
||||
let task: ModelEditTask;
|
||||
let makeMinimal = false;
|
||||
if (this._editor && this._editor.hasModel() && this._editor.getModel().uri.toString() === ref.object.textEditorModel.uri.toString()) {
|
||||
task = new EditorEditTask(ref, this._editor);
|
||||
makeMinimal = true;
|
||||
} else {
|
||||
task = new ModelEditTask(ref);
|
||||
}
|
||||
|
||||
for (const edit of value) {
|
||||
if (makeMinimal) {
|
||||
const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, [edit.edit]);
|
||||
if (!newEdits) {
|
||||
task.addEdit(edit);
|
||||
} else {
|
||||
for (let moreMinialEdit of newEdits) {
|
||||
task.addEdit({ ...edit, edit: moreMinialEdit });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
task.addEdit(edit);
|
||||
}
|
||||
}
|
||||
|
||||
this._tasks!.push(task);
|
||||
this._progress.report(undefined);
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
validate(): ValidationResult {
|
||||
for (const task of this._tasks!) {
|
||||
const result = task.validate();
|
||||
if (!result.canApply) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return { canApply: true };
|
||||
}
|
||||
|
||||
apply(): void {
|
||||
const tasks = this._tasks!;
|
||||
|
||||
if (tasks.length === 1) {
|
||||
// This edit touches a single model => keep things simple
|
||||
for (const task of tasks) {
|
||||
task.model.pushStackElement();
|
||||
task.apply();
|
||||
task.model.pushStackElement();
|
||||
this._progress.report(undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const multiModelEditStackElement = new MultiModelEditStackElement(
|
||||
this._label || localize('workspaceEdit', "Workspace Edit"),
|
||||
tasks.map(t => new SingleModelEditStackElement(t.model, t.getBeforeCursorState()))
|
||||
);
|
||||
this._undoRedoService.pushElement(multiModelEditStackElement);
|
||||
|
||||
for (const task of tasks) {
|
||||
task.apply();
|
||||
this._progress.report(undefined);
|
||||
}
|
||||
|
||||
multiModelEditStackElement.close();
|
||||
}
|
||||
}
|
||||
import { BulkTextEdits } from 'vs/workbench/services/bulkEdit/browser/bulkTextEdits';
|
||||
import { BulkFileEdits } from 'vs/workbench/services/bulkEdit/browser/bulkFileEdits';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
|
||||
type Edit = WorkspaceFileEdit | WorkspaceTextEdit;
|
||||
|
||||
@@ -257,10 +34,6 @@ class BulkEdit {
|
||||
edits: Edit[],
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@ITextFileService private readonly _textFileService: ITextFileService,
|
||||
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService
|
||||
) {
|
||||
this._label = label;
|
||||
this._editor = editor;
|
||||
@@ -282,7 +55,7 @@ class BulkEdit {
|
||||
|
||||
async perform(): Promise<void> {
|
||||
|
||||
let seen = new Set<string>();
|
||||
let seen = new ResourceMap<true>();
|
||||
let total = 0;
|
||||
|
||||
const groups: Edit[][] = [];
|
||||
@@ -299,8 +72,8 @@ class BulkEdit {
|
||||
|
||||
if (WorkspaceFileEdit.is(edit)) {
|
||||
total += 1;
|
||||
} else if (!seen.has(edit.resource.toString())) {
|
||||
seen.add(edit.resource.toString());
|
||||
} else if (!seen.has(edit.resource)) {
|
||||
seen.set(edit.resource, true);
|
||||
total += 2;
|
||||
}
|
||||
}
|
||||
@@ -323,55 +96,14 @@ class BulkEdit {
|
||||
|
||||
private async _performFileEdits(edits: WorkspaceFileEdit[], progress: IProgress<void>) {
|
||||
this._logService.debug('_performFileEdits', JSON.stringify(edits));
|
||||
for (const edit of edits) {
|
||||
progress.report(undefined);
|
||||
|
||||
let options = edit.options || {};
|
||||
|
||||
if (edit.newUri && edit.oldUri) {
|
||||
// rename
|
||||
if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.exists(edit.newUri)) {
|
||||
continue; // not overwriting, but ignoring, and the target file exists
|
||||
}
|
||||
await this._workingCopyFileService.move(edit.oldUri, edit.newUri, options.overwrite);
|
||||
|
||||
} else if (!edit.newUri && edit.oldUri) {
|
||||
// delete file
|
||||
if (await this._fileService.exists(edit.oldUri)) {
|
||||
let useTrash = this._configurationService.getValue<boolean>('files.enableTrash');
|
||||
if (useTrash && !(this._fileService.hasCapability(edit.oldUri, FileSystemProviderCapabilities.Trash))) {
|
||||
useTrash = false; // not supported by provider
|
||||
}
|
||||
await this._workingCopyFileService.delete(edit.oldUri, { useTrash, recursive: options.recursive });
|
||||
} else if (!options.ignoreIfNotExists) {
|
||||
throw new Error(`${edit.oldUri} does not exist and can not be deleted`);
|
||||
}
|
||||
} else if (edit.newUri && !edit.oldUri) {
|
||||
// create file
|
||||
if (options.overwrite === undefined && options.ignoreIfExists && await this._fileService.exists(edit.newUri)) {
|
||||
continue; // not overwriting, but ignoring, and the target file exists
|
||||
}
|
||||
await this._textFileService.create(edit.newUri, undefined, { overwrite: options.overwrite });
|
||||
}
|
||||
}
|
||||
const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), progress, edits);
|
||||
await model.apply();
|
||||
}
|
||||
|
||||
private async _performTextEdits(edits: WorkspaceTextEdit[], progress: IProgress<void>): Promise<void> {
|
||||
this._logService.debug('_performTextEdits', JSON.stringify(edits));
|
||||
|
||||
const model = this._instaService.createInstance(BulkEditModel, this._label, this._editor, progress, edits);
|
||||
|
||||
await model.prepare();
|
||||
|
||||
// this._throwIfConflicts(conflicts);
|
||||
const validationResult = model.validate();
|
||||
if (validationResult.canApply === false) {
|
||||
model.dispose();
|
||||
throw new Error(`${validationResult.reason.toString()} has changed in the meantime`);
|
||||
}
|
||||
|
||||
model.apply();
|
||||
model.dispose();
|
||||
const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._editor, progress, edits);
|
||||
await model.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,7 +116,6 @@ export class BulkEditService implements IBulkEditService {
|
||||
constructor(
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
) { }
|
||||
|
||||
@@ -413,18 +144,6 @@ export class BulkEditService implements IBulkEditService {
|
||||
|
||||
const { edits } = edit;
|
||||
let codeEditor = options?.editor;
|
||||
|
||||
// First check if loaded models were not changed in the meantime
|
||||
for (const edit of edits) {
|
||||
if (!WorkspaceFileEdit.is(edit) && typeof edit.modelVersionId === 'number') {
|
||||
let model = this._modelService.getModel(edit.resource);
|
||||
if (model && model.getVersionId() !== edit.modelVersionId) {
|
||||
// model changed in the meantime
|
||||
return Promise.reject(new Error(`${model.uri.toString()} has changed in the meantime`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try to find code editor
|
||||
if (!codeEditor) {
|
||||
let candidate = this._editorService.activeTextEditorControl;
|
||||
|
||||
180
src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts
Normal file
180
src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { WorkspaceFileEdit, WorkspaceFileEditOptions } from 'vs/editor/common/modes';
|
||||
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
|
||||
import { IProgress } from 'vs/platform/progress/common/progress';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
interface IFileOperation {
|
||||
uris: URI[];
|
||||
perform(): Promise<IFileOperation>;
|
||||
}
|
||||
|
||||
class Noop implements IFileOperation {
|
||||
readonly uris = [];
|
||||
async perform() { return this; }
|
||||
}
|
||||
|
||||
class RenameOperation implements IFileOperation {
|
||||
|
||||
constructor(
|
||||
readonly newUri: URI,
|
||||
readonly oldUri: URI,
|
||||
readonly options: WorkspaceFileEditOptions,
|
||||
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
) { }
|
||||
|
||||
get uris() {
|
||||
return [this.newUri, this.oldUri];
|
||||
}
|
||||
|
||||
async perform(): Promise<IFileOperation> {
|
||||
// rename
|
||||
if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) {
|
||||
return new Noop(); // not overwriting, but ignoring, and the target file exists
|
||||
}
|
||||
await this._workingCopyFileService.move([{ source: this.oldUri, target: this.newUri }], { overwrite: this.options.overwrite });
|
||||
return new RenameOperation(this.oldUri, this.newUri, this.options, this._workingCopyFileService, this._fileService);
|
||||
}
|
||||
}
|
||||
|
||||
class CreateOperation implements IFileOperation {
|
||||
|
||||
constructor(
|
||||
readonly newUri: URI,
|
||||
readonly options: WorkspaceFileEditOptions,
|
||||
readonly contents: VSBuffer | undefined,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
) { }
|
||||
|
||||
get uris() {
|
||||
return [this.newUri];
|
||||
}
|
||||
|
||||
async perform(): Promise<IFileOperation> {
|
||||
// create file
|
||||
if (this.options.overwrite === undefined && this.options.ignoreIfExists && await this._fileService.exists(this.newUri)) {
|
||||
return new Noop(); // not overwriting, but ignoring, and the target file exists
|
||||
}
|
||||
await this._workingCopyFileService.create(this.newUri, this.contents, { overwrite: this.options.overwrite });
|
||||
return this._instaService.createInstance(DeleteOperation, this.newUri, this.options);
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteOperation implements IFileOperation {
|
||||
|
||||
constructor(
|
||||
readonly oldUri: URI,
|
||||
readonly options: WorkspaceFileEditOptions,
|
||||
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
@ILogService private readonly _logService: ILogService
|
||||
) { }
|
||||
|
||||
get uris() {
|
||||
return [this.oldUri];
|
||||
}
|
||||
|
||||
async perform(): Promise<IFileOperation> {
|
||||
// delete file
|
||||
if (!await this._fileService.exists(this.oldUri)) {
|
||||
if (!this.options.ignoreIfNotExists) {
|
||||
throw new Error(`${this.oldUri} does not exist and can not be deleted`);
|
||||
}
|
||||
return new Noop();
|
||||
}
|
||||
|
||||
let contents: VSBuffer | undefined;
|
||||
try {
|
||||
contents = (await this._fileService.readFile(this.oldUri)).value;
|
||||
} catch (err) {
|
||||
this._logService.critical(err);
|
||||
}
|
||||
|
||||
const useTrash = this._fileService.hasCapability(this.oldUri, FileSystemProviderCapabilities.Trash) && this._configurationService.getValue<boolean>('files.enableTrash');
|
||||
await this._workingCopyFileService.delete([this.oldUri], { useTrash, recursive: this.options.recursive });
|
||||
return this._instaService.createInstance(CreateOperation, this.oldUri, this.options, contents);
|
||||
}
|
||||
}
|
||||
|
||||
class FileUndoRedoElement implements IWorkspaceUndoRedoElement {
|
||||
|
||||
readonly type = UndoRedoElementType.Workspace;
|
||||
|
||||
readonly resources: readonly URI[];
|
||||
|
||||
constructor(
|
||||
readonly label: string,
|
||||
readonly operations: IFileOperation[]
|
||||
) {
|
||||
this.resources = (<URI[]>[]).concat(...operations.map(op => op.uris));
|
||||
}
|
||||
|
||||
async undo(): Promise<void> {
|
||||
await this._reverse();
|
||||
}
|
||||
|
||||
async redo(): Promise<void> {
|
||||
await this._reverse();
|
||||
}
|
||||
|
||||
private async _reverse() {
|
||||
for (let i = 0; i < this.operations.length; i++) {
|
||||
const op = this.operations[i];
|
||||
const undo = await op.perform();
|
||||
this.operations[i] = undo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BulkFileEdits {
|
||||
|
||||
constructor(
|
||||
private readonly _label: string,
|
||||
private readonly _progress: IProgress<void>,
|
||||
private readonly _edits: WorkspaceFileEdit[],
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
|
||||
) { }
|
||||
|
||||
async apply(): Promise<void> {
|
||||
const undoOperations: IFileOperation[] = [];
|
||||
for (const edit of this._edits) {
|
||||
this._progress.report(undefined);
|
||||
|
||||
const options = edit.options || {};
|
||||
let op: IFileOperation | undefined;
|
||||
if (edit.newUri && edit.oldUri) {
|
||||
// rename
|
||||
op = this._instaService.createInstance(RenameOperation, edit.newUri, edit.oldUri, options);
|
||||
} else if (!edit.newUri && edit.oldUri) {
|
||||
// delete file
|
||||
op = this._instaService.createInstance(DeleteOperation, edit.oldUri, options);
|
||||
} else if (edit.newUri && !edit.oldUri) {
|
||||
// create file
|
||||
op = this._instaService.createInstance(CreateOperation, edit.newUri, options, undefined);
|
||||
}
|
||||
if (op) {
|
||||
const undoOp = await op.perform();
|
||||
undoOperations.push(undoOp);
|
||||
}
|
||||
}
|
||||
|
||||
this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, undoOperations));
|
||||
}
|
||||
}
|
||||
245
src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts
Normal file
245
src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mergeSort } from 'vs/base/common/arrays';
|
||||
import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model';
|
||||
import { WorkspaceTextEdit } from 'vs/editor/common/modes';
|
||||
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
import { IProgress } from 'vs/platform/progress/common/progress';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
|
||||
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
|
||||
type ValidationResult = { canApply: true } | { canApply: false, reason: URI };
|
||||
|
||||
class ModelEditTask implements IDisposable {
|
||||
|
||||
readonly model: ITextModel;
|
||||
|
||||
private _expectedModelVersionId: number | undefined;
|
||||
protected _edits: IIdentifiedSingleEditOperation[];
|
||||
protected _newEol: EndOfLineSequence | undefined;
|
||||
|
||||
constructor(private readonly _modelReference: IReference<IResolvedTextEditorModel>) {
|
||||
this.model = this._modelReference.object.textEditorModel;
|
||||
this._edits = [];
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._modelReference.dispose();
|
||||
}
|
||||
|
||||
addEdit(resourceEdit: WorkspaceTextEdit): void {
|
||||
this._expectedModelVersionId = resourceEdit.modelVersionId;
|
||||
const { edit } = resourceEdit;
|
||||
|
||||
if (typeof edit.eol === 'number') {
|
||||
// honor eol-change
|
||||
this._newEol = edit.eol;
|
||||
}
|
||||
if (!edit.range && !edit.text) {
|
||||
// lacks both a range and the text
|
||||
return;
|
||||
}
|
||||
if (Range.isEmpty(edit.range) && !edit.text) {
|
||||
// no-op edit (replace empty range with empty text)
|
||||
return;
|
||||
}
|
||||
|
||||
// create edit operation
|
||||
let range: Range;
|
||||
if (!edit.range) {
|
||||
range = this.model.getFullModelRange();
|
||||
} else {
|
||||
range = Range.lift(edit.range);
|
||||
}
|
||||
this._edits.push(EditOperation.replaceMove(range, edit.text));
|
||||
}
|
||||
|
||||
validate(): ValidationResult {
|
||||
if (typeof this._expectedModelVersionId === 'undefined' || this.model.getVersionId() === this._expectedModelVersionId) {
|
||||
return { canApply: true };
|
||||
}
|
||||
return { canApply: false, reason: this.model.uri };
|
||||
}
|
||||
|
||||
getBeforeCursorState(): Selection[] | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
apply(): void {
|
||||
if (this._edits.length > 0) {
|
||||
this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range));
|
||||
this.model.pushEditOperations(null, this._edits, () => null);
|
||||
}
|
||||
if (this._newEol !== undefined) {
|
||||
this.model.pushEOL(this._newEol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EditorEditTask extends ModelEditTask {
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
|
||||
constructor(modelReference: IReference<IResolvedTextEditorModel>, editor: ICodeEditor) {
|
||||
super(modelReference);
|
||||
this._editor = editor;
|
||||
}
|
||||
|
||||
getBeforeCursorState(): Selection[] | null {
|
||||
return this._editor.getSelections();
|
||||
}
|
||||
|
||||
apply(): void {
|
||||
if (this._edits.length > 0) {
|
||||
this._edits = mergeSort(this._edits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range));
|
||||
this._editor.executeEdits('', this._edits);
|
||||
}
|
||||
if (this._newEol !== undefined) {
|
||||
if (this._editor.hasModel()) {
|
||||
this._editor.getModel().pushEOL(this._newEol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BulkTextEdits {
|
||||
|
||||
private readonly _edits = new ResourceMap<WorkspaceTextEdit[]>();
|
||||
|
||||
constructor(
|
||||
private readonly _label: string,
|
||||
private readonly _editor: ICodeEditor | undefined,
|
||||
private readonly _progress: IProgress<void>,
|
||||
edits: WorkspaceTextEdit[],
|
||||
@IEditorWorkerService private readonly _editorWorker: IEditorWorkerService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@ITextModelService private readonly _textModelResolverService: ITextModelService,
|
||||
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService
|
||||
) {
|
||||
|
||||
for (const edit of edits) {
|
||||
let array = this._edits.get(edit.resource);
|
||||
if (!array) {
|
||||
array = [];
|
||||
this._edits.set(edit.resource, array);
|
||||
}
|
||||
array.push(edit);
|
||||
}
|
||||
}
|
||||
|
||||
private _validateBeforePrepare(): void {
|
||||
// First check if loaded models were not changed in the meantime
|
||||
for (const array of this._edits.values()) {
|
||||
for (let edit of array) {
|
||||
if (typeof edit.modelVersionId === 'number') {
|
||||
let model = this._modelService.getModel(edit.resource);
|
||||
if (model && model.getVersionId() !== edit.modelVersionId) {
|
||||
// model changed in the meantime
|
||||
throw new Error(`${model.uri.toString()} has changed in the meantime`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _createEditsTasks(): Promise<ModelEditTask[]> {
|
||||
|
||||
const tasks: ModelEditTask[] = [];
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
for (let [key, value] of this._edits) {
|
||||
const promise = this._textModelResolverService.createModelReference(key).then(async ref => {
|
||||
let task: ModelEditTask;
|
||||
let makeMinimal = false;
|
||||
if (this._editor?.getModel()?.uri.toString() === ref.object.textEditorModel.uri.toString()) {
|
||||
task = new EditorEditTask(ref, this._editor);
|
||||
makeMinimal = true;
|
||||
} else {
|
||||
task = new ModelEditTask(ref);
|
||||
}
|
||||
|
||||
for (const edit of value) {
|
||||
if (makeMinimal) {
|
||||
const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, [edit.edit]);
|
||||
if (!newEdits) {
|
||||
task.addEdit(edit);
|
||||
} else {
|
||||
for (let moreMinialEdit of newEdits) {
|
||||
task.addEdit({ ...edit, edit: moreMinialEdit });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
task.addEdit(edit);
|
||||
}
|
||||
}
|
||||
|
||||
tasks.push(task);
|
||||
this._progress.report(undefined);
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
return tasks;
|
||||
}
|
||||
|
||||
private _validateTasks(tasks: ModelEditTask[]): ValidationResult {
|
||||
for (const task of tasks) {
|
||||
const result = task.validate();
|
||||
if (!result.canApply) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return { canApply: true };
|
||||
}
|
||||
|
||||
async apply(): Promise<void> {
|
||||
|
||||
this._validateBeforePrepare();
|
||||
const tasks = await this._createEditsTasks();
|
||||
|
||||
try {
|
||||
|
||||
const validation = this._validateTasks(tasks);
|
||||
if (!validation.canApply) {
|
||||
throw new Error(`${(validation as { canApply: false, reason: URI }).reason.toString()} has changed in the meantime`); // {{SQL CARBON EDIT}} strict-null-checks
|
||||
}
|
||||
if (tasks.length === 1) {
|
||||
// This edit touches a single model => keep things simple
|
||||
for (const task of tasks) {
|
||||
task.model.pushStackElement();
|
||||
task.apply();
|
||||
task.model.pushStackElement();
|
||||
this._progress.report(undefined);
|
||||
}
|
||||
} else {
|
||||
// prepare multi model undo element
|
||||
const multiModelEditStackElement = new MultiModelEditStackElement(
|
||||
this._label,
|
||||
tasks.map(t => new SingleModelEditStackElement(t.model, t.getBeforeCursorState()))
|
||||
);
|
||||
this._undoRedoService.pushElement(multiModelEditStackElement);
|
||||
for (const task of tasks) {
|
||||
task.apply();
|
||||
this._progress.report(undefined);
|
||||
}
|
||||
multiModelEditStackElement.close();
|
||||
}
|
||||
|
||||
} finally {
|
||||
dispose(tasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,11 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { clipboard } from 'electron';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
export class NativeClipboardService implements IClipboardService {
|
||||
|
||||
@@ -52,13 +52,12 @@ export class NativeClipboardService implements IClipboardService {
|
||||
return this.bufferToResources(await this.electronService.readClipboardBuffer(NativeClipboardService.FILE_FORMAT));
|
||||
}
|
||||
|
||||
|
||||
async hasResources(): Promise<boolean> {
|
||||
return this.electronService.hasClipboard(NativeClipboardService.FILE_FORMAT);
|
||||
}
|
||||
|
||||
private resourcesToBuffer(resources: URI[]): Buffer {
|
||||
return Buffer.from(resources.map(r => r.toString()).join('\n'));
|
||||
private resourcesToBuffer(resources: URI[]): Uint8Array {
|
||||
return VSBuffer.fromString(resources.map(r => r.toString()).join('\n')).buffer;
|
||||
}
|
||||
|
||||
private bufferToResources(buffer: Uint8Array): URI[] {
|
||||
@@ -77,22 +76,6 @@ export class NativeClipboardService implements IClipboardService {
|
||||
return []; // do not trust clipboard data
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
readFindTextSync(): string {
|
||||
if (isMacintosh) {
|
||||
return clipboard.readFindText();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
writeFindTextSync(text: string): void {
|
||||
if (isMacintosh) {
|
||||
clipboard.writeFindText(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IClipboardService, NativeClipboardService, true);
|
||||
@@ -1807,7 +1807,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => {
|
||||
return promise;
|
||||
});
|
||||
|
||||
test('update remote settings', async () => {
|
||||
test.skip('update remote settings', async () => {
|
||||
registerRemoteFileSystemProvider();
|
||||
resolveRemoteEnvironment();
|
||||
await initialize();
|
||||
|
||||
@@ -208,7 +208,8 @@ class DecorationProviderWrapper {
|
||||
constructor(
|
||||
readonly provider: IDecorationsProvider,
|
||||
private readonly _uriEmitter: Emitter<URI | URI[]>,
|
||||
private readonly _flushEmitter: Emitter<IResourceDecorationChangeEvent>
|
||||
private readonly _flushEmitter: Emitter<IResourceDecorationChangeEvent>,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
) {
|
||||
this._dispoable = this.provider.onDidChange(uris => {
|
||||
if (!uris) {
|
||||
@@ -238,16 +239,17 @@ class DecorationProviderWrapper {
|
||||
}
|
||||
|
||||
getOrRetrieve(uri: URI, includeChildren: boolean, callback: (data: IDecorationData, isChild: boolean) => void): void {
|
||||
|
||||
let item = this.data.get(uri);
|
||||
|
||||
if (item === undefined) {
|
||||
// unknown -> trigger request
|
||||
this._logService.trace('[Decorations] getOrRetrieve -> FETCH', this.provider.label, uri);
|
||||
item = this._fetchData(uri);
|
||||
}
|
||||
|
||||
if (item && !(item instanceof DecorationDataRequest)) {
|
||||
// found something (which isn't pending anymore)
|
||||
this._logService.trace('[Decorations] getOrRetrieve -> RESULT', this.provider.label, uri);
|
||||
callback(item, false);
|
||||
}
|
||||
|
||||
@@ -257,6 +259,7 @@ class DecorationProviderWrapper {
|
||||
if (iter) {
|
||||
for (let item = iter.next(); !item.done; item = iter.next()) {
|
||||
if (item.value && !(item.value instanceof DecorationDataRequest)) {
|
||||
this._logService.trace('[Decorations] getOrRetrieve -> RESULT (children)', this.provider.label, uri);
|
||||
callback(item.value, true);
|
||||
}
|
||||
}
|
||||
@@ -269,6 +272,7 @@ class DecorationProviderWrapper {
|
||||
// check for pending request and cancel it
|
||||
const pendingRequest = this.data.get(uri);
|
||||
if (pendingRequest instanceof DecorationDataRequest) {
|
||||
this._logService.trace('[Decorations] fetchData -> CANCEL previous', this.provider.label, uri);
|
||||
pendingRequest.source.cancel();
|
||||
this.data.delete(uri);
|
||||
}
|
||||
@@ -297,6 +301,7 @@ class DecorationProviderWrapper {
|
||||
}
|
||||
|
||||
private _keepItem(uri: URI, data: IDecorationData | undefined): IDecorationData | null {
|
||||
this._logService.trace('[Decorations] keepItem -> CANCEL previous', this.provider.label, uri, data);
|
||||
const deco = data ? data : null;
|
||||
const old = this.data.set(uri, deco);
|
||||
if (deco || old) {
|
||||
@@ -343,7 +348,8 @@ export class DecorationsService implements IDecorationsService {
|
||||
const wrapper = new DecorationProviderWrapper(
|
||||
provider,
|
||||
this._onDidChangeDecorationsDelayed,
|
||||
this._onDidChangeDecorations
|
||||
this._onDidChangeDecorations,
|
||||
this._logService
|
||||
);
|
||||
const remove = this._data.push(wrapper);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { coalesce } from 'vs/base/common/arrays';
|
||||
import { trim } from 'vs/base/common/strings';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
|
||||
export abstract class AbstractFileDialogService implements IFileDialogService {
|
||||
|
||||
@@ -218,21 +219,17 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
|
||||
}
|
||||
|
||||
private pickResource(options: IOpenDialogOptions): Promise<URI | undefined> {
|
||||
const simpleFileDialog = this.createSimpleFileDialog();
|
||||
const simpleFileDialog = this.instantiationService.createInstance(SimpleFileDialog);
|
||||
|
||||
return simpleFileDialog.showOpenDialog(options);
|
||||
}
|
||||
|
||||
private saveRemoteResource(options: ISaveDialogOptions): Promise<URI | undefined> {
|
||||
const remoteFileDialog = this.createSimpleFileDialog();
|
||||
const remoteFileDialog = this.instantiationService.createInstance(SimpleFileDialog);
|
||||
|
||||
return remoteFileDialog.showSaveDialog(options);
|
||||
}
|
||||
|
||||
protected createSimpleFileDialog(): SimpleFileDialog {
|
||||
return this.instantiationService.createInstance(SimpleFileDialog);
|
||||
}
|
||||
|
||||
protected getSchemeFilterForWindow(): string {
|
||||
return !this.environmentService.configuration.remoteAuthority ? Schemas.file : REMOTE_HOST_SCHEME;
|
||||
}
|
||||
@@ -254,7 +251,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
|
||||
const options: ISaveDialogOptions = {
|
||||
defaultUri,
|
||||
title: nls.localize('saveAsTitle', "Save As"),
|
||||
availableFileSystems,
|
||||
availableFileSystems
|
||||
};
|
||||
|
||||
interface IFilter { name: string; extensions: string[]; }
|
||||
@@ -282,8 +279,12 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
|
||||
// We have no matching filter, e.g. because the language
|
||||
// is unknown. We still add the extension to the list of
|
||||
// filters though so that it can be picked
|
||||
// (https://github.com/microsoft/vscode/issues/96283)
|
||||
if (!matchingFilter && ext) {
|
||||
// (https://github.com/microsoft/vscode/issues/96283) but
|
||||
// only on Windows where this is an issue. Adding this to
|
||||
// macOS would result in the following bugs:
|
||||
// https://github.com/microsoft/vscode/issues/100614 and
|
||||
// https://github.com/microsoft/vscode/issues/100241
|
||||
if (isWindows && !matchingFilter && ext) {
|
||||
matchingFilter = { name: trim(ext, '.').toUpperCase(), extensions: [trim(ext, '.')] };
|
||||
}
|
||||
|
||||
|
||||
@@ -231,8 +231,8 @@ export class SimpleFileDialog {
|
||||
return this.remoteAgentEnvironment;
|
||||
}
|
||||
|
||||
protected async getUserHome(): Promise<URI> {
|
||||
return (await this.pathService.userHome) ?? URI.from({ scheme: this.scheme, authority: this.remoteAuthority, path: '/' });
|
||||
protected getUserHome(): Promise<URI> {
|
||||
return this.pathService.userHome({ preferLocal: this.scheme === Schemas.file });
|
||||
}
|
||||
|
||||
private async pickResource(isSave: boolean = false): Promise<URI | undefined> {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { SimpleFileDialog } from 'vs/workbench/services/dialogs/browser/simpleFileDialog';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IPathService } from 'vs/workbench/services/path/common/pathService';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
|
||||
|
||||
export class NativeSimpleFileDialog extends SimpleFileDialog {
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IQuickInputService quickInputService: IQuickInputService,
|
||||
@ILabelService labelService: ILabelService,
|
||||
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IFileDialogService fileDialogService: IFileDialogService,
|
||||
@IModelService modelService: IModelService,
|
||||
@IModeService modeService: IModeService,
|
||||
@IWorkbenchEnvironmentService protected environmentService: INativeWorkbenchEnvironmentService,
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
|
||||
@IPathService protected pathService: IPathService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
super(fileService, quickInputService, labelService, workspaceContextService, notificationService, fileDialogService, modelService, modeService, environmentService, remoteAgentService, pathService, keybindingService, contextKeyService);
|
||||
}
|
||||
|
||||
protected async getUserHome(): Promise<URI> {
|
||||
if (this.scheme !== Schemas.file) {
|
||||
return super.getUserHome();
|
||||
}
|
||||
return this.environmentService.userHome;
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,6 @@ import { Schemas } from 'vs/base/common/network';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { SimpleFileDialog } from 'vs/workbench/services/dialogs/browser/simpleFileDialog';
|
||||
import { NativeSimpleFileDialog } from 'vs/workbench/services/dialogs/electron-browser/simpleFileDialog';
|
||||
|
||||
export class FileDialogService extends AbstractFileDialogService implements IFileDialogService {
|
||||
|
||||
@@ -191,10 +189,6 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
|
||||
// Don't allow untitled schema through.
|
||||
return schema === Schemas.untitled ? [Schemas.file] : (schema !== Schemas.file ? [schema, Schemas.file] : [schema]);
|
||||
}
|
||||
|
||||
protected createSimpleFileDialog(): SimpleFileDialog {
|
||||
return this.instantiationService.createInstance(NativeSimpleFileDialog);
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IFileDialogService, FileDialogService, true);
|
||||
@@ -12,6 +12,7 @@ import { TextEditorOptions } from 'vs/workbench/common/editor';
|
||||
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { extUri } from 'vs/base/common/resources';
|
||||
|
||||
export class CodeEditorService extends CodeEditorServiceImpl {
|
||||
|
||||
@@ -47,13 +48,13 @@ export class CodeEditorService extends CodeEditorServiceImpl {
|
||||
// side as separate editor.
|
||||
const activeTextEditorControl = this.editorService.activeTextEditorControl;
|
||||
if (
|
||||
!sideBySide && // we need the current active group to be the taret
|
||||
isDiffEditor(activeTextEditorControl) && // we only support this for active text diff editors
|
||||
input.options && // we need options to apply
|
||||
input.resource && // we need a request resource to compare with
|
||||
activeTextEditorControl.getModel() && // we need a target model to compare with
|
||||
source === activeTextEditorControl.getModifiedEditor() && // we need the source of this request to be the modified side of the diff editor
|
||||
input.resource.toString() === activeTextEditorControl.getModel()!.modified.uri.toString() // we need the input resources to match with modified side
|
||||
!sideBySide && // we need the current active group to be the taret
|
||||
isDiffEditor(activeTextEditorControl) && // we only support this for active text diff editors
|
||||
input.options && // we need options to apply
|
||||
input.resource && // we need a request resource to compare with
|
||||
activeTextEditorControl.getModel() && // we need a target model to compare with
|
||||
source === activeTextEditorControl.getModifiedEditor() && // we need the source of this request to be the modified side of the diff editor
|
||||
extUri.isEqual(input.resource, activeTextEditorControl.getModel()!.modified.uri) // we need the input resources to match with modified side
|
||||
) {
|
||||
const targetEditor = activeTextEditorControl.getModifiedEditor();
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IEditorDropTargetDelegate } from 'vs/workbench/browser/parts/editor/editorDropTarget';
|
||||
|
||||
export const IEditorDropService = createDecorator<IEditorDropService>('editorDropService');
|
||||
|
||||
export interface IEditorDropService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* Allows to register a drag and drop target for editors.
|
||||
*/
|
||||
createEditorDropTarget(container: HTMLElement, delegate: IEditorDropTargetDelegate): IDisposable;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/u
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { indexOfPath } from 'vs/base/common/extpath';
|
||||
import { DEFAULT_CUSTOM_EDITOR, updateViewTypeSchema, editorAssociationsConfigurationNode } from 'vs/workbench/services/editor/common/editorAssociationsSetting';
|
||||
import { DEFAULT_CUSTOM_EDITOR, updateViewTypeSchema, editorAssociationsConfigurationNode } from 'vs/workbench/services/editor/common/editorOpenWith';
|
||||
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
|
||||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
@@ -185,8 +185,8 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
|
||||
for (const editor of this.visibleEditors) {
|
||||
const resources = distinct(coalesce([
|
||||
toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }),
|
||||
toResource(editor, { supportSideBySide: SideBySideEditor.DETAILS })
|
||||
toResource(editor, { supportSideBySide: SideBySideEditor.PRIMARY }),
|
||||
toResource(editor, { supportSideBySide: SideBySideEditor.SECONDARY })
|
||||
]), resource => resource.toString());
|
||||
|
||||
for (const resource of resources) {
|
||||
@@ -248,7 +248,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
|
||||
// Determine new resulting target resource
|
||||
let targetResource: URI;
|
||||
if (source.toString() === resource.toString()) {
|
||||
if (extUri.isEqual(source, resource)) {
|
||||
targetResource = target; // file got moved
|
||||
} else {
|
||||
const ignoreCase = !this.fileService.hasCapability(resource, FileSystemProviderCapabilities.PathCaseSensitive);
|
||||
@@ -380,8 +380,8 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
|
||||
for (const editor of this.editors) {
|
||||
if (options.supportSideBySide && editor instanceof SideBySideEditorInput) {
|
||||
conditionallyAddEditor(editor.master);
|
||||
conditionallyAddEditor(editor.details);
|
||||
conditionallyAddEditor(editor.primary);
|
||||
conditionallyAddEditor(editor.secondary);
|
||||
} else {
|
||||
conditionallyAddEditor(editor);
|
||||
}
|
||||
@@ -1193,13 +1193,13 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
|
||||
return new Promise(resolve => {
|
||||
const listener = this.onDidCloseEditor(async event => {
|
||||
const detailsResource = toResource(event.editor, { supportSideBySide: SideBySideEditor.DETAILS });
|
||||
const masterResource = toResource(event.editor, { supportSideBySide: SideBySideEditor.MASTER });
|
||||
const primaryResource = toResource(event.editor, { supportSideBySide: SideBySideEditor.PRIMARY });
|
||||
const secondaryResource = toResource(event.editor, { supportSideBySide: SideBySideEditor.SECONDARY });
|
||||
|
||||
// Remove from resources to wait for being closed based on the
|
||||
// resources from editors that got closed
|
||||
remainingEditors = remainingEditors.filter(({ resource }) => {
|
||||
if (this.uriIdentityService.extUri.isEqual(resource, masterResource) || this.uriIdentityService.extUri.isEqual(resource, detailsResource)) {
|
||||
if (this.uriIdentityService.extUri.isEqual(resource, primaryResource) || this.uriIdentityService.extUri.isEqual(resource, secondaryResource)) {
|
||||
return false; // remove - the closing editor matches this resource
|
||||
}
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IConfigurationNode, IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { ICustomEditorInfo } from 'vs/workbench/services/editor/common/editorService';
|
||||
|
||||
export const customEditorsAssociationsSettingId = 'workbench.editorAssociations';
|
||||
|
||||
export const viewTypeSchamaAddition: IJSONSchema = {
|
||||
type: 'string',
|
||||
enum: []
|
||||
};
|
||||
|
||||
export type CustomEditorAssociation = {
|
||||
readonly viewType: string;
|
||||
readonly filenamePattern?: string;
|
||||
};
|
||||
|
||||
export type CustomEditorsAssociations = readonly CustomEditorAssociation[];
|
||||
|
||||
export const editorAssociationsConfigurationNode: IConfigurationNode = {
|
||||
...workbenchConfigurationNodeBase,
|
||||
properties: {
|
||||
[customEditorsAssociationsSettingId]: {
|
||||
type: 'array',
|
||||
markdownDescription: nls.localize('editor.editorAssociations', "Configure which editor to use for specific file types."),
|
||||
items: {
|
||||
type: 'object',
|
||||
defaultSnippets: [{
|
||||
body: {
|
||||
'viewType': '$1',
|
||||
'filenamePattern': '$2'
|
||||
}
|
||||
}],
|
||||
properties: {
|
||||
'viewType': {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
description: nls.localize('editor.editorAssociations.viewType', "The unique id of the editor to use."),
|
||||
},
|
||||
viewTypeSchamaAddition
|
||||
]
|
||||
},
|
||||
'filenamePattern': {
|
||||
type: 'string',
|
||||
description: nls.localize('editor.editorAssociations.filenamePattern', "Glob pattern specifying which files the editor should be used for."),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const builtinProviderDisplayName = nls.localize('builtinProviderDisplayName', "Built-in");
|
||||
|
||||
export const DEFAULT_CUSTOM_EDITOR: ICustomEditorInfo = {
|
||||
id: 'default',
|
||||
displayName: nls.localize('promptOpenWith.defaultEditor.displayName', "Text Editor"),
|
||||
providerDisplayName: builtinProviderDisplayName
|
||||
};
|
||||
|
||||
export function updateViewTypeSchema(enumValues: string[], enumDescriptions: string[]): void {
|
||||
viewTypeSchamaAddition.enum = enumValues;
|
||||
viewTypeSchamaAddition.enumDescriptions = enumDescriptions;
|
||||
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration)
|
||||
.notifyConfigurationSchemaUpdated(editorAssociationsConfigurationNode);
|
||||
}
|
||||
219
src/vs/workbench/services/editor/common/editorOpenWith.ts
Normal file
219
src/vs/workbench/services/editor/common/editorOpenWith.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { IConfigurationNode, IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { ICustomEditorInfo, IEditorService, IOpenEditorOverrideHandler, IOpenEditorOverrideEntry } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorInput, IEditorPane, IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor';
|
||||
import { ITextEditorOptions, IEditorOptions } from 'vs/platform/editor/common/editor';
|
||||
import { IEditorGroup, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { extname, basename, isEqual } from 'vs/base/common/resources';
|
||||
|
||||
/**
|
||||
* Id of the default editor for open with.
|
||||
*/
|
||||
export const DEFAULT_EDITOR_ID = 'default';
|
||||
|
||||
/**
|
||||
* Try to open an resource with a given editor.
|
||||
*
|
||||
* @param input Resource to open.
|
||||
* @param id Id of the editor to use. If not provided, the user is prompted for which editor to use.
|
||||
*/
|
||||
export async function openEditorWith(
|
||||
input: IEditorInput,
|
||||
id: string | undefined,
|
||||
options: IEditorOptions | ITextEditorOptions | undefined,
|
||||
group: IEditorGroup,
|
||||
editorService: IEditorService,
|
||||
configurationService: IConfigurationService,
|
||||
quickInputService: IQuickInputService,
|
||||
): Promise<IEditorPane | undefined> {
|
||||
const resource = input.resource;
|
||||
if (!resource) {
|
||||
return undefined; // {{SQL CARBON EDIT}} strict-null-checks
|
||||
}
|
||||
|
||||
const allEditorOverrides = getAllAvailableEditors(resource, options, group, editorService);
|
||||
if (!allEditorOverrides.length) {
|
||||
return undefined; // {{SQL CARBON EDIT}} strict-null-checks
|
||||
}
|
||||
|
||||
const overrideToUse = typeof id === 'string' && allEditorOverrides.find(([_, entry]) => entry.id === id);
|
||||
if (overrideToUse) {
|
||||
return overrideToUse[0].open(input, { ...options, override: id }, group, OpenEditorContext.NEW_EDITOR)?.override;
|
||||
}
|
||||
|
||||
// Prompt
|
||||
const resourceExt = extname(resource);
|
||||
|
||||
const items: (IQuickPickItem & { handler: IOpenEditorOverrideHandler })[] = allEditorOverrides.map((override) => {
|
||||
return {
|
||||
handler: override[0],
|
||||
id: override[1].id,
|
||||
label: override[1].label,
|
||||
description: override[1].active ? nls.localize('promptOpenWith.currentlyActive', 'Currently Active') : undefined,
|
||||
detail: override[1].detail,
|
||||
buttons: resourceExt ? [{
|
||||
iconClass: 'codicon-settings-gear',
|
||||
tooltip: nls.localize('promptOpenWith.setDefaultTooltip', "Set as default editor for '{0}' files", resourceExt)
|
||||
}] : undefined
|
||||
};
|
||||
});
|
||||
|
||||
const picker = quickInputService.createQuickPick<(IQuickPickItem & { handler: IOpenEditorOverrideHandler })>();
|
||||
picker.items = items;
|
||||
if (items.length) {
|
||||
picker.selectedItems = [items[0]];
|
||||
}
|
||||
picker.placeholder = nls.localize('promptOpenWith.placeHolder', "Select editor for '{0}'", basename(resource));
|
||||
|
||||
const pickedItem = await new Promise<(IQuickPickItem & { handler: IOpenEditorOverrideHandler }) | undefined>(resolve => {
|
||||
picker.onDidAccept(() => {
|
||||
resolve(picker.selectedItems.length === 1 ? picker.selectedItems[0] : undefined);
|
||||
picker.dispose();
|
||||
});
|
||||
|
||||
picker.onDidTriggerItemButton(e => {
|
||||
const pick = e.item;
|
||||
const id = pick.id;
|
||||
resolve(pick); // open the view
|
||||
picker.dispose();
|
||||
|
||||
// And persist the setting
|
||||
if (pick && id) {
|
||||
const newAssociation: CustomEditorAssociation = { viewType: id, filenamePattern: '*' + resourceExt };
|
||||
const currentAssociations = [...configurationService.getValue<CustomEditorsAssociations>(customEditorsAssociationsSettingId)];
|
||||
|
||||
// First try updating existing association
|
||||
for (let i = 0; i < currentAssociations.length; ++i) {
|
||||
const existing = currentAssociations[i];
|
||||
if (existing.filenamePattern === newAssociation.filenamePattern) {
|
||||
currentAssociations.splice(i, 1, newAssociation);
|
||||
configurationService.updateValue(customEditorsAssociationsSettingId, currentAssociations);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, create a new one
|
||||
currentAssociations.unshift(newAssociation);
|
||||
configurationService.updateValue(customEditorsAssociationsSettingId, currentAssociations);
|
||||
}
|
||||
});
|
||||
|
||||
picker.show();
|
||||
});
|
||||
|
||||
return pickedItem?.handler.open(input, { ...options, override: pickedItem.id }, group, OpenEditorContext.NEW_EDITOR)?.override;
|
||||
}
|
||||
|
||||
const builtinProviderDisplayName = nls.localize('builtinProviderDisplayName', "Built-in");
|
||||
|
||||
export const defaultEditorOverrideEntry = Object.freeze({
|
||||
id: DEFAULT_EDITOR_ID,
|
||||
label: nls.localize('promptOpenWith.defaultEditor.displayName', "Text Editor"),
|
||||
detail: builtinProviderDisplayName
|
||||
});
|
||||
|
||||
/**
|
||||
* Get a list of all available editors, including the default text editor.
|
||||
*/
|
||||
export function getAllAvailableEditors(
|
||||
resource: URI,
|
||||
options: IEditorOptions | ITextEditorOptions | undefined,
|
||||
group: IEditorGroup,
|
||||
editorService: IEditorService
|
||||
): Array<[IOpenEditorOverrideHandler, IOpenEditorOverrideEntry]> {
|
||||
const fileEditorInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).getFileEditorInputFactory();
|
||||
const overrides = editorService.getEditorOverrides(resource, options, group);
|
||||
if (!overrides.some(([_, entry]) => entry.id === DEFAULT_EDITOR_ID)) {
|
||||
overrides.unshift([
|
||||
{
|
||||
open: (input: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup) => {
|
||||
if (!input.resource) {
|
||||
return undefined; // {{SQL CARBON EDIT}} strict-null-checks
|
||||
}
|
||||
|
||||
const fileEditorInput = editorService.createEditorInput({ resource: input.resource, forceFile: true });
|
||||
const textOptions: IEditorOptions | ITextEditorOptions = options ? { ...options, override: false } : { override: false };
|
||||
return { override: editorService.openEditor(fileEditorInput, textOptions, group) };
|
||||
}
|
||||
},
|
||||
{
|
||||
...defaultEditorOverrideEntry,
|
||||
active: fileEditorInputFactory.isFileEditorInput(editorService.activeEditor) && isEqual(editorService.activeEditor.resource, resource),
|
||||
}]);
|
||||
}
|
||||
|
||||
return overrides;
|
||||
}
|
||||
|
||||
export const customEditorsAssociationsSettingId = 'workbench.editorAssociations';
|
||||
|
||||
export const viewTypeSchamaAddition: IJSONSchema = {
|
||||
type: 'string',
|
||||
enum: []
|
||||
};
|
||||
|
||||
export type CustomEditorAssociation = {
|
||||
readonly viewType: string;
|
||||
readonly filenamePattern?: string;
|
||||
};
|
||||
|
||||
export type CustomEditorsAssociations = readonly CustomEditorAssociation[];
|
||||
|
||||
export const editorAssociationsConfigurationNode: IConfigurationNode = {
|
||||
...workbenchConfigurationNodeBase,
|
||||
properties: {
|
||||
[customEditorsAssociationsSettingId]: {
|
||||
type: 'array',
|
||||
markdownDescription: nls.localize('editor.editorAssociations', "Configure which editor to use for specific file types."),
|
||||
items: {
|
||||
type: 'object',
|
||||
defaultSnippets: [{
|
||||
body: {
|
||||
'viewType': '$1',
|
||||
'filenamePattern': '$2'
|
||||
}
|
||||
}],
|
||||
properties: {
|
||||
'viewType': {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
description: nls.localize('editor.editorAssociations.viewType', "The unique id of the editor to use."),
|
||||
},
|
||||
viewTypeSchamaAddition
|
||||
]
|
||||
},
|
||||
'filenamePattern': {
|
||||
type: 'string',
|
||||
description: nls.localize('editor.editorAssociations.filenamePattern', "Glob pattern specifying which files the editor should be used for."),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const DEFAULT_CUSTOM_EDITOR: ICustomEditorInfo = {
|
||||
id: 'default',
|
||||
displayName: nls.localize('promptOpenWith.defaultEditor.displayName', "Text Editor"),
|
||||
providerDisplayName: builtinProviderDisplayName
|
||||
};
|
||||
|
||||
export function updateViewTypeSchema(enumValues: string[], enumDescriptions: string[]): void {
|
||||
viewTypeSchamaAddition.enum = enumValues;
|
||||
viewTypeSchamaAddition.enumDescriptions = enumDescriptions;
|
||||
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration)
|
||||
.notifyConfigurationSchemaUpdated(editorAssociationsConfigurationNode);
|
||||
}
|
||||
@@ -74,7 +74,6 @@ export interface ISaveAllEditorsOptions extends ISaveEditorsOptions, IBaseSaveRe
|
||||
export interface IRevertAllEditorsOptions extends IRevertOptions, IBaseSaveRevertAllEditorOptions { }
|
||||
|
||||
export interface ICustomEditorInfo {
|
||||
|
||||
readonly id: string;
|
||||
readonly displayName: string;
|
||||
readonly providerDisplayName: string;
|
||||
@@ -82,6 +81,7 @@ export interface ICustomEditorInfo {
|
||||
|
||||
export interface ICustomEditorViewTypesHandler {
|
||||
readonly onDidChangeViewTypes: Event<void>;
|
||||
|
||||
getViewTypes(): ICustomEditorInfo[];
|
||||
}
|
||||
|
||||
|
||||
@@ -377,8 +377,8 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
|
||||
|
||||
// Untyped Input (diff)
|
||||
input = service.createEditorInput({
|
||||
leftResource: toResource.call(this, '/master.html'),
|
||||
rightResource: toResource.call(this, '/detail.html')
|
||||
leftResource: toResource.call(this, '/primary.html'),
|
||||
rightResource: toResource.call(this, '/secondary.html')
|
||||
});
|
||||
assert(input instanceof DiffEditorInput);
|
||||
});
|
||||
@@ -1084,7 +1084,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
|
||||
const editor = await service.openEditor(input1, { pinned: true });
|
||||
await service.openEditor(input2, { pinned: true });
|
||||
|
||||
const whenClosed = service.whenClosed([input1, input2]);
|
||||
const whenClosed = service.whenClosed([{ resource: input1.resource }, { resource: input2.resource }]);
|
||||
|
||||
editor?.group?.closeAllEditors();
|
||||
|
||||
|
||||
@@ -62,12 +62,12 @@ export class BrowserEnvironmentConfiguration implements IEnvironmentConfiguratio
|
||||
@memoize
|
||||
get filesToDiff(): IPath[] | undefined {
|
||||
if (this.payload) {
|
||||
const fileToDiffDetail = this.payload.get('diffFileDetail');
|
||||
const fileToDiffMaster = this.payload.get('diffFileMaster');
|
||||
if (fileToDiffDetail && fileToDiffMaster) {
|
||||
const fileToDiffPrimary = this.payload.get('diffFilePrimary');
|
||||
const fileToDiffSecondary = this.payload.get('diffFileSecondary');
|
||||
if (fileToDiffPrimary && fileToDiffSecondary) {
|
||||
return [
|
||||
{ fileUri: URI.parse(fileToDiffDetail) },
|
||||
{ fileUri: URI.parse(fileToDiffMaster) }
|
||||
{ fileUri: URI.parse(fileToDiffSecondary) },
|
||||
{ fileUri: URI.parse(fileToDiffPrimary) }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ export interface INativeWorkbenchEnvironmentService extends IWorkbenchEnvironmen
|
||||
|
||||
readonly log?: string;
|
||||
readonly extHostLogsPath: URI;
|
||||
|
||||
readonly userHome: URI;
|
||||
}
|
||||
|
||||
export interface INativeEnvironmentConfiguration extends IEnvironmentConfiguration, INativeWindowConfiguration { }
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
|
||||
interface IScannedBuiltinExtension {
|
||||
extensionPath: string,
|
||||
packageJSON: IExtensionManifest,
|
||||
packageNLSPath?: string,
|
||||
readmePath?: string,
|
||||
changelogPath?: string,
|
||||
}
|
||||
|
||||
export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScannerService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly builtinExtensions: IScannedExtension[] = [];
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IUriIdentityService uriIdentityService: IUriIdentityService,
|
||||
) {
|
||||
|
||||
const builtinExtensionsServiceUrl = environmentService.options?.builtinExtensionsServiceUrl ? URI.parse(environmentService.options?.builtinExtensionsServiceUrl) : undefined;
|
||||
if (isWeb && builtinExtensionsServiceUrl) {
|
||||
|
||||
let scannedBuiltinExtensions: IScannedBuiltinExtension[] = [];
|
||||
|
||||
if (environmentService.isBuilt) {
|
||||
// Built time configuration (do NOT modify)
|
||||
scannedBuiltinExtensions = [/*BUILD->INSERT_BUILTIN_EXTENSIONS*/];
|
||||
} else {
|
||||
// Find builtin extensions by checking for DOM
|
||||
const builtinExtensionsElement = document.getElementById('vscode-workbench-builtin-extensions');
|
||||
const builtinExtensionsElementAttribute = builtinExtensionsElement ? builtinExtensionsElement.getAttribute('data-settings') : undefined;
|
||||
if (builtinExtensionsElementAttribute) {
|
||||
try {
|
||||
scannedBuiltinExtensions = JSON.parse(builtinExtensionsElementAttribute);
|
||||
} catch (error) { /* ignore error*/ }
|
||||
}
|
||||
}
|
||||
|
||||
this.builtinExtensions = scannedBuiltinExtensions.map(e => ({
|
||||
identifier: { id: getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name) },
|
||||
location: uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.extensionPath),
|
||||
type: ExtensionType.System,
|
||||
packageJSON: e.packageJSON,
|
||||
packageNLSUrl: e.packageNLSPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.packageNLSPath) : undefined,
|
||||
readmeUrl: e.readmePath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.readmePath) : undefined,
|
||||
changelogUrl: e.changelogPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.changelogPath) : undefined,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async scanBuiltinExtensions(): Promise<IScannedExtension[]> {
|
||||
if (isWeb) {
|
||||
return this.builtinExtensions;
|
||||
}
|
||||
throw new Error('not supported');
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IBuiltinExtensionsScannerService, BuiltinExtensionsScannerService);
|
||||
@@ -18,6 +18,7 @@ import { getExtensionKind } from 'vs/workbench/services/extensions/common/extens
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { StorageManager } from 'vs/platform/extensionManagement/common/extensionEnablementService';
|
||||
import { webWorkerExtHostConfig } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
const SOURCE = 'IWorkbenchExtensionEnablementService';
|
||||
|
||||
@@ -137,8 +138,8 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
|
||||
}
|
||||
|
||||
private _isDisabledByExtensionKind(extension: IExtension): boolean {
|
||||
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
|
||||
const server = this.extensionManagementServerService.getExtensionManagementServer(extension.location);
|
||||
if (this.extensionManagementServerService.remoteExtensionManagementServer || this.extensionManagementServerService.webExtensionManagementServer) {
|
||||
const server = this.extensionManagementServerService.getExtensionManagementServer(extension);
|
||||
for (const extensionKind of getExtensionKind(extension.manifest, this.productService, this.configurationService)) {
|
||||
if (extensionKind === 'ui') {
|
||||
if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer === server) {
|
||||
@@ -151,8 +152,13 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench
|
||||
}
|
||||
}
|
||||
if (extensionKind === 'web') {
|
||||
// Web extensions are not yet supported to be disabled by kind. Enable them always on web.
|
||||
const enableLocalWebWorker = this.configurationService.getValue<boolean>(webWorkerExtHostConfig);
|
||||
if (enableLocalWebWorker) {
|
||||
// Web extensions are enabled on all configurations
|
||||
return false;
|
||||
}
|
||||
if (this.extensionManagementServerService.localExtensionManagementServer === null) {
|
||||
// Web extensions run only in the web
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,24 +6,25 @@
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IExtension, IScannedExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
|
||||
export const IExtensionManagementServerService = createDecorator<IExtensionManagementServerService>('extensionManagementServerService');
|
||||
|
||||
export interface IExtensionManagementServer {
|
||||
extensionManagementService: IExtensionManagementService;
|
||||
authority: string;
|
||||
id: string;
|
||||
label: string;
|
||||
extensionManagementService: IExtensionManagementService;
|
||||
}
|
||||
|
||||
export interface IExtensionManagementServerService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly localExtensionManagementServer: IExtensionManagementServer | null;
|
||||
readonly remoteExtensionManagementServer: IExtensionManagementServer | null;
|
||||
getExtensionManagementServer(location: URI): IExtensionManagementServer | null;
|
||||
readonly webExtensionManagementServer: IExtensionManagementServer | null;
|
||||
getExtensionManagementServer(extension: IExtension): IExtensionManagementServer | null;
|
||||
}
|
||||
|
||||
export const enum EnablementState {
|
||||
@@ -139,3 +140,11 @@ export interface IExtensionRecommendationsService {
|
||||
getRecommendedExtensionsByScenario(scenarioType: string): Promise<IExtensionRecommendation[]>; // {{SQL CARBON EDIT}}
|
||||
promptRecommendedExtensionsByScenario(scenarioType: string): void; // {{SQL CARBON EDIT}}
|
||||
}
|
||||
|
||||
export const IWebExtensionsScannerService = createDecorator<IWebExtensionsScannerService>('IWebExtensionsScannerService');
|
||||
export interface IWebExtensionsScannerService {
|
||||
readonly _serviceBrand: undefined;
|
||||
scanExtensions(type?: ExtensionType): Promise<IScannedExtension[]>;
|
||||
addExtension(galleryExtension: IGalleryExtension): Promise<IScannedExtension>;
|
||||
removeExtension(identifier: IExtensionIdentifier, version?: string): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
@@ -12,6 +11,10 @@ import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { WebExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/webExtensionManagementService';
|
||||
import { IExtension } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
export class ExtensionManagementServerService implements IExtensionManagementServerService {
|
||||
|
||||
@@ -19,27 +22,40 @@ export class ExtensionManagementServerService implements IExtensionManagementSer
|
||||
|
||||
readonly localExtensionManagementServer: IExtensionManagementServer | null = null;
|
||||
readonly remoteExtensionManagementServer: IExtensionManagementServer | null = null;
|
||||
readonly webExtensionManagementServer: IExtensionManagementServer | null = null;
|
||||
|
||||
constructor(
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
|
||||
@ILabelService labelService: ILabelService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
) {
|
||||
const remoteAgentConnection = remoteAgentService.getConnection();
|
||||
if (remoteAgentConnection) {
|
||||
const extensionManagementService = new ExtensionManagementChannelClient(remoteAgentConnection!.getChannel<IChannel>('extensions'));
|
||||
this.remoteExtensionManagementServer = {
|
||||
authority: remoteAgentConnection.remoteAuthority,
|
||||
id: 'remote',
|
||||
extensionManagementService,
|
||||
get label() { return labelService.getHostLabel(REMOTE_HOST_SCHEME, remoteAgentConnection!.remoteAuthority) || localize('remote', "Remote"); }
|
||||
};
|
||||
}
|
||||
if (isWeb) {
|
||||
const extensionManagementService = instantiationService.createInstance(WebExtensionManagementService);
|
||||
this.webExtensionManagementServer = {
|
||||
id: 'web',
|
||||
extensionManagementService,
|
||||
label: localize('web', "Web")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getExtensionManagementServer(location: URI): IExtensionManagementServer | null {
|
||||
if (location.scheme === REMOTE_HOST_SCHEME) {
|
||||
return this.remoteExtensionManagementServer;
|
||||
getExtensionManagementServer(extension: IExtension): IExtensionManagementServer {
|
||||
if (extension.location.scheme === REMOTE_HOST_SCHEME) {
|
||||
return this.remoteExtensionManagementServer!;
|
||||
}
|
||||
return null;
|
||||
if (this.webExtensionManagementServer) {
|
||||
return this.webExtensionManagementServer;
|
||||
}
|
||||
throw new Error(`Invalid Extension ${extension.location}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { localize } from 'vs/nls';
|
||||
import { prefersExecuteOnUI, canExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||
import { prefersExecuteOnUI, canExecuteOnWorkspace, prefersExecuteOnWorkspace, canExecuteOnUI, prefersExecuteOnWeb, canExecuteOnWeb } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IDownloadService } from 'vs/platform/download/common/download';
|
||||
@@ -45,6 +45,9 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
|
||||
this.servers.push(this.extensionManagementServerService.remoteExtensionManagementServer);
|
||||
}
|
||||
if (this.extensionManagementServerService.webExtensionManagementServer) {
|
||||
this.servers.push(this.extensionManagementServerService.webExtensionManagementServer);
|
||||
}
|
||||
|
||||
this.onInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer<InstallExtensionEvent>, server) => { emitter.add(server.extensionManagementService.onInstallExtension); return emitter; }, new EventMultiplexer<InstallExtensionEvent>())).event;
|
||||
this.onDidInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer<DidInstallExtensionEvent>, server) => { emitter.add(server.extensionManagementService.onDidInstallExtension); return emitter; }, new EventMultiplexer<DidInstallExtensionEvent>())).event;
|
||||
@@ -64,7 +67,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
if (!server) {
|
||||
return Promise.reject(`Invalid location ${extension.location.toString()}`);
|
||||
}
|
||||
if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) {
|
||||
if (this.servers.length > 1) {
|
||||
if (isLanguagePackExtension(extension.manifest)) {
|
||||
return this.uninstallEverywhere(extension);
|
||||
}
|
||||
@@ -79,12 +82,14 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
return Promise.reject(`Invalid location ${extension.location.toString()}`);
|
||||
}
|
||||
const promise = server.extensionManagementService.uninstall(extension);
|
||||
const anotherServer: IExtensionManagementServer | null = server === this.extensionManagementServerService.localExtensionManagementServer ? this.extensionManagementServerService.remoteExtensionManagementServer! : this.extensionManagementServerService.localExtensionManagementServer;
|
||||
if (anotherServer) {
|
||||
const installed = await anotherServer.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
extension = installed.filter(i => areSameExtensions(i.identifier, extension.identifier))[0];
|
||||
if (extension) {
|
||||
await anotherServer.extensionManagementService.uninstall(extension);
|
||||
const otherServers: IExtensionManagementServer[] = this.servers.filter(s => s !== server);
|
||||
if (otherServers.length) {
|
||||
for (const otherServer of otherServers) {
|
||||
const installed = await otherServer.extensionManagementService.getInstalled(ExtensionType.User);
|
||||
extension = installed.filter(i => areSameExtensions(i.identifier, extension.identifier))[0];
|
||||
if (extension) {
|
||||
await otherServer.extensionManagementService.uninstall(extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
return promise;
|
||||
@@ -141,7 +146,10 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
}
|
||||
|
||||
unzip(zipLocation: URI): Promise<IExtensionIdentifier> {
|
||||
return Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation))).then(([extensionIdentifier]) => extensionIdentifier);
|
||||
return Promise.all(this.servers
|
||||
// Filter out web server
|
||||
.filter(server => server !== this.extensionManagementServerService.webExtensionManagementServer)
|
||||
.map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation))).then(([extensionIdentifier]) => extensionIdentifier);
|
||||
}
|
||||
|
||||
async install(vsix: URI): Promise<ILocalExtension> {
|
||||
@@ -149,7 +157,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
const manifest = await this.getManifest(vsix);
|
||||
if (isLanguagePackExtension(manifest)) {
|
||||
// Install on both servers
|
||||
const [local] = await Promise.all(this.servers.map(server => this.installVSIX(vsix, server)));
|
||||
const [local] = await Promise.all([this.extensionManagementServerService.localExtensionManagementServer, this.extensionManagementServerService.remoteExtensionManagementServer].map(server => this.installVSIX(vsix, server)));
|
||||
return local;
|
||||
}
|
||||
if (prefersExecuteOnUI(manifest, this.productService, this.configurationService)) {
|
||||
@@ -183,39 +191,61 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
}
|
||||
|
||||
async installFromGallery(gallery: IGalleryExtension): Promise<ILocalExtension> {
|
||||
if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) {
|
||||
const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None);
|
||||
if (manifest) {
|
||||
if (isLanguagePackExtension(manifest)) {
|
||||
// Install on both servers
|
||||
return Promise.all(this.servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(([local]) => local);
|
||||
}
|
||||
if (prefersExecuteOnUI(manifest, this.productService, this.configurationService)) {
|
||||
// Install only on local server
|
||||
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
}
|
||||
// Install only on remote server
|
||||
return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
} else {
|
||||
return Promise.reject(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name));
|
||||
}
|
||||
}
|
||||
if (this.extensionManagementServerService.localExtensionManagementServer) {
|
||||
|
||||
// Only local server, install without any checks
|
||||
if (this.servers.length === 1 && this.extensionManagementServerService.localExtensionManagementServer) {
|
||||
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
}
|
||||
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
|
||||
const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None);
|
||||
if (!manifest) {
|
||||
return Promise.reject(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name));
|
||||
}
|
||||
if (!isLanguagePackExtension(manifest) && !canExecuteOnWorkspace(manifest, this.productService, this.configurationService)) {
|
||||
const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", gallery.displayName || gallery.name));
|
||||
error.name = INSTALL_ERROR_NOT_SUPPORTED;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None);
|
||||
if (!manifest) {
|
||||
return Promise.reject(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name));
|
||||
}
|
||||
|
||||
// Install Language pack on all servers
|
||||
if (isLanguagePackExtension(manifest)) {
|
||||
return Promise.all(this.servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(([local]) => local);
|
||||
}
|
||||
|
||||
// 1. Install on preferred location
|
||||
|
||||
// Install UI preferred extension on local server
|
||||
if (prefersExecuteOnUI(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.localExtensionManagementServer) {
|
||||
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
}
|
||||
// Install Workspace preferred extension on remote server
|
||||
if (prefersExecuteOnWorkspace(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.remoteExtensionManagementServer) {
|
||||
return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
}
|
||||
return Promise.reject('No Servers to Install');
|
||||
// Install Web preferred extension on web server
|
||||
if (prefersExecuteOnWeb(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.webExtensionManagementServer) {
|
||||
return this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
}
|
||||
|
||||
// 2. Install on supported location
|
||||
|
||||
// Install UI supported extension on local server
|
||||
if (canExecuteOnUI(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.localExtensionManagementServer) {
|
||||
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
}
|
||||
// Install Workspace supported extension on remote server
|
||||
if (canExecuteOnWorkspace(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.remoteExtensionManagementServer) {
|
||||
return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
}
|
||||
// Install Web supported extension on web server
|
||||
if (canExecuteOnWeb(manifest, this.productService, this.configurationService) && this.extensionManagementServerService.webExtensionManagementServer) {
|
||||
return this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
|
||||
}
|
||||
|
||||
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
|
||||
const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", gallery.displayName || gallery.name));
|
||||
error.name = INSTALL_ERROR_NOT_SUPPORTED;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const error = new Error(localize('cannot be installed on web', "Cannot install '{0}' because this extension has defined that it cannot run on the web server.", gallery.displayName || gallery.name));
|
||||
error.name = INSTALL_ERROR_NOT_SUPPORTED;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
getExtensionsReport(): Promise<IReportedExtension[]> {
|
||||
@@ -229,6 +259,6 @@ export class ExtensionManagementService extends Disposable implements IExtension
|
||||
}
|
||||
|
||||
private getServer(extension: ILocalExtension): IExtensionManagementServer | null {
|
||||
return this.extensionManagementServerService.getExtensionManagementServer(extension.location);
|
||||
return this.extensionManagementServerService.getExtensionManagementServer(extension);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ExtensionType, IExtensionIdentifier, IExtensionManifest, IScannedExtension } from 'vs/platform/extensions/common/extensions';
|
||||
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IGalleryExtension, IReportedExtension, IGalleryMetadata, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IRequestService, isSuccess, asText } from 'vs/platform/request/common/request';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class WebExtensionManagementService extends Disposable implements IExtensionManagementService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onInstallExtension = this._register(new Emitter<InstallExtensionEvent>());
|
||||
readonly onInstallExtension: Event<InstallExtensionEvent> = this._onInstallExtension.event;
|
||||
|
||||
private readonly _onDidInstallExtension = this._register(new Emitter<DidInstallExtensionEvent>());
|
||||
readonly onDidInstallExtension: Event<DidInstallExtensionEvent> = this._onDidInstallExtension.event;
|
||||
|
||||
private readonly _onUninstallExtension = this._register(new Emitter<IExtensionIdentifier>());
|
||||
readonly onUninstallExtension: Event<IExtensionIdentifier> = this._onUninstallExtension.event;
|
||||
|
||||
private _onDidUninstallExtension = this._register(new Emitter<DidUninstallExtensionEvent>());
|
||||
onDidUninstallExtension: Event<DidUninstallExtensionEvent> = this._onDidUninstallExtension.event;
|
||||
|
||||
constructor(
|
||||
@IWebExtensionsScannerService private readonly webExtensionsScannerService: IWebExtensionsScannerService,
|
||||
@IRequestService private readonly requestService: IRequestService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getInstalled(type?: ExtensionType): Promise<ILocalExtension[]> {
|
||||
const extensions = await this.webExtensionsScannerService.scanExtensions(type);
|
||||
return Promise.all(extensions.map(e => this.toLocalExtension(e)));
|
||||
}
|
||||
|
||||
async installFromGallery(gallery: IGalleryExtension): Promise<ILocalExtension> {
|
||||
this.logService.info('Installing extension:', gallery.identifier.id);
|
||||
this._onInstallExtension.fire({ identifier: gallery.identifier, gallery });
|
||||
try {
|
||||
const existingExtension = await this.getUserExtension(gallery.identifier);
|
||||
if (existingExtension && existingExtension.manifest.version !== gallery.version) {
|
||||
await this.webExtensionsScannerService.removeExtension(existingExtension.identifier, existingExtension.manifest.version);
|
||||
}
|
||||
const scannedExtension = await this.webExtensionsScannerService.addExtension(gallery);
|
||||
const local = await this.toLocalExtension(scannedExtension);
|
||||
this._onDidInstallExtension.fire({ local, identifier: gallery.identifier, operation: InstallOperation.Install, gallery });
|
||||
return local;
|
||||
} catch (error) {
|
||||
this._onDidInstallExtension.fire({ error, identifier: gallery.identifier, operation: InstallOperation.Install, gallery });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(extension: ILocalExtension): Promise<void> {
|
||||
this._onUninstallExtension.fire(extension.identifier);
|
||||
try {
|
||||
await this.webExtensionsScannerService.removeExtension(extension.identifier);
|
||||
this._onDidUninstallExtension.fire({ identifier: extension.identifier });
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
this._onDidUninstallExtension.fire({ error, identifier: extension.identifier });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise<ILocalExtension> {
|
||||
return local;
|
||||
}
|
||||
|
||||
private async getUserExtension(identifier: IExtensionIdentifier): Promise<ILocalExtension | undefined> {
|
||||
const userExtensions = await this.getInstalled(ExtensionType.User);
|
||||
return userExtensions.find(e => areSameExtensions(e.identifier, identifier));
|
||||
}
|
||||
|
||||
private async toLocalExtension(scannedExtension: IScannedExtension): Promise<ILocalExtension> {
|
||||
let manifest = scannedExtension.packageJSON;
|
||||
if (scannedExtension.packageNLSUrl) {
|
||||
try {
|
||||
const context = await this.requestService.request({ type: 'GET', url: scannedExtension.packageNLSUrl.toString() }, CancellationToken.None);
|
||||
if (isSuccess(context)) {
|
||||
const content = await asText(context);
|
||||
if (content) {
|
||||
manifest = localizeManifest(manifest, JSON.parse(content));
|
||||
}
|
||||
}
|
||||
} catch (error) { /* ignore */ }
|
||||
}
|
||||
return <ILocalExtension>{
|
||||
type: scannedExtension.type,
|
||||
identifier: scannedExtension.identifier,
|
||||
manifest,
|
||||
location: scannedExtension.location,
|
||||
isMachineScoped: false,
|
||||
publisherId: null,
|
||||
publisherDisplayName: null
|
||||
};
|
||||
}
|
||||
|
||||
zip(extension: ILocalExtension): Promise<URI> { throw new Error('unsupported'); }
|
||||
unzip(zipLocation: URI): Promise<IExtensionIdentifier> { throw new Error('unsupported'); }
|
||||
getManifest(vsix: URI): Promise<IExtensionManifest> { throw new Error('unsupported'); }
|
||||
install(vsix: URI, isMachineScoped?: boolean): Promise<ILocalExtension> { throw new Error('unsupported'); }
|
||||
reinstallFromGallery(extension: ILocalExtension): Promise<void> { throw new Error('unsupported'); }
|
||||
getExtensionsReport(): Promise<IReportedExtension[]> { throw new Error('unsupported'); }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as semver from 'semver-umd';
|
||||
import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { Queue } from 'vs/base/common/async';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { asText, isSuccess, IRequestService } from 'vs/platform/request/common/request';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { groupByExtension, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
|
||||
interface IUserExtension {
|
||||
identifier: IExtensionIdentifier;
|
||||
version: string;
|
||||
uri: URI;
|
||||
readmeUri?: URI;
|
||||
changelogUri?: URI;
|
||||
packageNLSUri?: URI;
|
||||
}
|
||||
|
||||
interface IStoredUserExtension {
|
||||
identifier: IExtensionIdentifier;
|
||||
version: string;
|
||||
uri: UriComponents;
|
||||
readmeUri?: UriComponents;
|
||||
changelogUri?: UriComponents;
|
||||
packageNLSUri?: UriComponents;
|
||||
}
|
||||
|
||||
const AssetTypeWebResource = 'Microsoft.VisualStudio.Code.WebResources';
|
||||
|
||||
function getExtensionLocation(assetUri: URI): URI { return joinPath(assetUri, AssetTypeWebResource, 'extension'); }
|
||||
|
||||
export class WebExtensionsScannerService implements IWebExtensionsScannerService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly systemExtensionsPromise: Promise<IScannedExtension[]>;
|
||||
private readonly staticExtensions: IScannedExtension[];
|
||||
private readonly extensionsResource: URI;
|
||||
private readonly userExtensionsResourceLimiter: Queue<IUserExtension[]>;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IBuiltinExtensionsScannerService private readonly builtinExtensionsScannerService: IBuiltinExtensionsScannerService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IRequestService private readonly requestService: IRequestService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
this.extensionsResource = joinPath(environmentService.userRoamingDataHome, 'extensions.json');
|
||||
this.userExtensionsResourceLimiter = new Queue<IUserExtension[]>();
|
||||
this.systemExtensionsPromise = isWeb ? this.builtinExtensionsScannerService.scanBuiltinExtensions() : Promise.resolve([]);
|
||||
const staticExtensions = environmentService.options && Array.isArray(environmentService.options.staticExtensions) ? environmentService.options.staticExtensions : [];
|
||||
this.staticExtensions = staticExtensions.map(data => <IScannedExtension>{
|
||||
location: data.extensionLocation,
|
||||
type: ExtensionType.User,
|
||||
packageJSON: data.packageJSON,
|
||||
});
|
||||
}
|
||||
|
||||
async scanExtensions(type?: ExtensionType): Promise<IScannedExtension[]> {
|
||||
const extensions = [];
|
||||
if (type === undefined || type === ExtensionType.System) {
|
||||
const systemExtensions = await this.systemExtensionsPromise;
|
||||
extensions.push(...systemExtensions);
|
||||
}
|
||||
if (type === undefined || type === ExtensionType.User) {
|
||||
extensions.push(...this.staticExtensions);
|
||||
const userExtensions = await this.scanUserExtensions();
|
||||
extensions.push(...userExtensions);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
async addExtension(galleryExtension: IGalleryExtension): Promise<IScannedExtension> {
|
||||
if (!galleryExtension.assetTypes.some(type => type.startsWith(AssetTypeWebResource))) {
|
||||
throw new Error(`Missing ${AssetTypeWebResource} asset type`);
|
||||
}
|
||||
|
||||
const packageNLSUri = joinPath(getExtensionLocation(galleryExtension.assetUri), 'package.nls.json');
|
||||
const context = await this.requestService.request({ type: 'GET', url: packageNLSUri.toString() }, CancellationToken.None);
|
||||
const packageNLSExists = isSuccess(context);
|
||||
|
||||
const userExtensions = await this.readUserExtensions();
|
||||
const userExtension: IUserExtension = {
|
||||
identifier: galleryExtension.identifier,
|
||||
version: galleryExtension.version,
|
||||
uri: galleryExtension.assetUri,
|
||||
readmeUri: galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined,
|
||||
changelogUri: galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined,
|
||||
packageNLSUri: packageNLSExists ? packageNLSUri : undefined
|
||||
};
|
||||
userExtensions.push(userExtension);
|
||||
await this.writeUserExtensions(userExtensions);
|
||||
|
||||
const scannedExtension = await this.toScannedExtension(userExtension);
|
||||
if (scannedExtension) {
|
||||
return scannedExtension;
|
||||
}
|
||||
throw new Error('Error while scanning extension');
|
||||
}
|
||||
|
||||
async removeExtension(identifier: IExtensionIdentifier, version?: string): Promise<void> {
|
||||
let userExtensions = await this.readUserExtensions();
|
||||
userExtensions = userExtensions.filter(extension => !(areSameExtensions(extension.identifier, identifier) && version ? extension.version === version : true));
|
||||
await this.writeUserExtensions(userExtensions);
|
||||
}
|
||||
|
||||
private async scanUserExtensions(): Promise<IScannedExtension[]> {
|
||||
let userExtensions = await this.readUserExtensions();
|
||||
const byExtension: IUserExtension[][] = groupByExtension(userExtensions, e => e.identifier);
|
||||
userExtensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]);
|
||||
const scannedExtensions: IScannedExtension[] = [];
|
||||
await Promise.all(userExtensions.map(async userExtension => {
|
||||
try {
|
||||
const scannedExtension = await this.toScannedExtension(userExtension);
|
||||
if (scannedExtension) {
|
||||
scannedExtensions.push(scannedExtension);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error, 'Error while scanning user extension', userExtension.identifier.id);
|
||||
}
|
||||
}));
|
||||
return scannedExtensions;
|
||||
}
|
||||
|
||||
private async toScannedExtension(userExtension: IUserExtension): Promise<IScannedExtension | null> {
|
||||
const context = await this.requestService.request({ type: 'GET', url: joinPath(userExtension.uri, 'Microsoft.VisualStudio.Code.Manifest').toString() }, CancellationToken.None);
|
||||
if (isSuccess(context)) {
|
||||
const content = await asText(context);
|
||||
if (content) {
|
||||
const packageJSON = JSON.parse(content);
|
||||
return {
|
||||
identifier: userExtension.identifier,
|
||||
location: getExtensionLocation(userExtension.uri),
|
||||
packageJSON,
|
||||
type: ExtensionType.User,
|
||||
readmeUrl: userExtension.readmeUri,
|
||||
changelogUrl: userExtension.changelogUri,
|
||||
packageNLSUrl: userExtension.packageNLSUri,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private readUserExtensions(): Promise<IUserExtension[]> {
|
||||
return this.userExtensionsResourceLimiter.queue(async () => {
|
||||
try {
|
||||
const content = await this.fileService.readFile(this.extensionsResource);
|
||||
const storedUserExtensions: IStoredUserExtension[] = JSON.parse(content.value.toString());
|
||||
return storedUserExtensions.map(e => ({
|
||||
identifier: e.identifier,
|
||||
version: e.version,
|
||||
uri: URI.revive(e.uri),
|
||||
readmeUri: URI.revive(e.readmeUri),
|
||||
changelogUri: URI.revive(e.changelogUri),
|
||||
packageNLSUri: URI.revive(e.packageNLSUri),
|
||||
}));
|
||||
} catch (error) { /* Ignore */ }
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
private writeUserExtensions(userExtensions: IUserExtension[]): Promise<IUserExtension[]> {
|
||||
return this.userExtensionsResourceLimiter.queue(async () => {
|
||||
const storedUserExtensions: IStoredUserExtension[] = userExtensions.map(e => ({
|
||||
identifier: e.identifier,
|
||||
version: e.version,
|
||||
uri: e.uri.toJSON(),
|
||||
readmeUri: e.readmeUri?.toJSON(),
|
||||
changelogUri: e.changelogUri?.toJSON(),
|
||||
packageNLSUri: e.packageNLSUri?.toJSON(),
|
||||
}));
|
||||
await this.fileService.writeFile(this.extensionsResource, VSBuffer.fromString(JSON.stringify(storedUserExtensions)));
|
||||
return userExtensions;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IWebExtensionsScannerService, WebExtensionsScannerService);
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
|
||||
@@ -19,19 +18,16 @@ import { RemoteExtensionManagementChannelClient } from 'vs/workbench/services/ex
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
|
||||
const localExtensionManagementServerAuthority: string = 'vscode-local';
|
||||
import { IExtension } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
export class ExtensionManagementServerService implements IExtensionManagementServerService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _localExtensionManagementServer: IExtensionManagementServer;
|
||||
public get localExtensionManagementServer(): IExtensionManagementServer {
|
||||
return this._localExtensionManagementServer;
|
||||
}
|
||||
public get localExtensionManagementServer(): IExtensionManagementServer { return this._localExtensionManagementServer; }
|
||||
readonly remoteExtensionManagementServer: IExtensionManagementServer | null = null;
|
||||
readonly isSingleServer: boolean = false;
|
||||
readonly webExtensionManagementServer: IExtensionManagementServer | null = null;
|
||||
|
||||
constructor(
|
||||
@ISharedProcessService sharedProcessService: ISharedProcessService,
|
||||
@@ -44,26 +40,26 @@ export class ExtensionManagementServerService implements IExtensionManagementSer
|
||||
) {
|
||||
const localExtensionManagementService = new ExtensionManagementChannelClient(sharedProcessService.getChannel('extensions'));
|
||||
|
||||
this._localExtensionManagementServer = { extensionManagementService: localExtensionManagementService, authority: localExtensionManagementServerAuthority, label: localize('local', "Local") };
|
||||
this._localExtensionManagementServer = { extensionManagementService: localExtensionManagementService, id: 'local', label: localize('local', "Local") };
|
||||
const remoteAgentConnection = remoteAgentService.getConnection();
|
||||
if (remoteAgentConnection) {
|
||||
const extensionManagementService = new RemoteExtensionManagementChannelClient(remoteAgentConnection.getChannel<IChannel>('extensions'), this.localExtensionManagementServer.extensionManagementService, galleryService, logService, configurationService, productService);
|
||||
this.remoteExtensionManagementServer = {
|
||||
authority: remoteAgentConnection.remoteAuthority,
|
||||
id: 'remote',
|
||||
extensionManagementService,
|
||||
get label() { return labelService.getHostLabel(REMOTE_HOST_SCHEME, remoteAgentConnection!.remoteAuthority) || localize('remote', "Remote"); }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getExtensionManagementServer(location: URI): IExtensionManagementServer | null {
|
||||
if (location.scheme === Schemas.file) {
|
||||
getExtensionManagementServer(extension: IExtension): IExtensionManagementServer {
|
||||
if (extension.location.scheme === Schemas.file) {
|
||||
return this.localExtensionManagementServer;
|
||||
}
|
||||
if (location.scheme === REMOTE_HOST_SCHEME) {
|
||||
if (this.remoteExtensionManagementServer && extension.location.scheme === REMOTE_HOST_SCHEME) {
|
||||
return this.remoteExtensionManagementServer;
|
||||
}
|
||||
return null;
|
||||
throw new Error(`Invalid Extension ${extension.location}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -461,7 +461,7 @@ suite('ExtensionEnablementService Test', () => {
|
||||
});
|
||||
|
||||
test('test remote ui extension is disabled by kind when there is no local server', async () => {
|
||||
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService)));
|
||||
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService), null));
|
||||
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
|
||||
testObject = new TestExtensionEnablementService(instantiationService);
|
||||
assert.ok(!testObject.isEnabled(localWorkspaceExtension));
|
||||
@@ -499,7 +499,7 @@ suite('ExtensionEnablementService Test', () => {
|
||||
});
|
||||
|
||||
test('test web extension on remote server is not disabled by kind when there is no local server', async () => {
|
||||
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService)));
|
||||
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService)));
|
||||
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
|
||||
testObject = new TestExtensionEnablementService(instantiationService);
|
||||
assert.ok(testObject.isEnabled(localWorkspaceExtension));
|
||||
@@ -507,7 +507,7 @@ suite('ExtensionEnablementService Test', () => {
|
||||
});
|
||||
|
||||
test('test web extension with no server is not disabled by kind when there is no local server', async () => {
|
||||
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService)));
|
||||
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService)));
|
||||
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.https }) });
|
||||
testObject = new TestExtensionEnablementService(instantiationService);
|
||||
assert.ok(testObject.isEnabled(localWorkspaceExtension));
|
||||
@@ -515,7 +515,7 @@ suite('ExtensionEnablementService Test', () => {
|
||||
});
|
||||
|
||||
test('test web extension with no server is not disabled by kind when there is no local and remote server', async () => {
|
||||
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, null));
|
||||
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, null, anExtensionManagementServer('web', instantiationService)));
|
||||
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.https }) });
|
||||
testObject = new TestExtensionEnablementService(instantiationService);
|
||||
assert.ok(testObject.isEnabled(localWorkspaceExtension));
|
||||
@@ -526,7 +526,7 @@ suite('ExtensionEnablementService Test', () => {
|
||||
|
||||
function anExtensionManagementServer(authority: string, instantiationService: TestInstantiationService): IExtensionManagementServer {
|
||||
return {
|
||||
authority,
|
||||
id: authority,
|
||||
label: authority,
|
||||
extensionManagementService: instantiationService.get(IExtensionManagementService)
|
||||
};
|
||||
@@ -535,22 +535,23 @@ function anExtensionManagementServer(authority: string, instantiationService: Te
|
||||
function aMultiExtensionManagementServerService(instantiationService: TestInstantiationService): IExtensionManagementServerService {
|
||||
const localExtensionManagementServer = anExtensionManagementServer('vscode-local', instantiationService);
|
||||
const remoteExtensionManagementServer = anExtensionManagementServer('vscode-remote', instantiationService);
|
||||
return anExtensionManagementServerService(localExtensionManagementServer, remoteExtensionManagementServer);
|
||||
return anExtensionManagementServerService(localExtensionManagementServer, remoteExtensionManagementServer, null);
|
||||
}
|
||||
|
||||
function anExtensionManagementServerService(localExtensionManagementServer: IExtensionManagementServer | null, remoteExtensionManagementServer: IExtensionManagementServer | null): IExtensionManagementServerService {
|
||||
function anExtensionManagementServerService(localExtensionManagementServer: IExtensionManagementServer | null, remoteExtensionManagementServer: IExtensionManagementServer | null, webExtensionManagementServer: IExtensionManagementServer | null): IExtensionManagementServerService {
|
||||
return {
|
||||
_serviceBrand: undefined,
|
||||
localExtensionManagementServer,
|
||||
remoteExtensionManagementServer,
|
||||
getExtensionManagementServer: (location: URI) => {
|
||||
if (location.scheme === Schemas.file) {
|
||||
webExtensionManagementServer: null,
|
||||
getExtensionManagementServer: (extension: IExtension) => {
|
||||
if (extension.location.scheme === Schemas.file) {
|
||||
return localExtensionManagementServer;
|
||||
}
|
||||
if (location.scheme === REMOTE_HOST_SCHEME) {
|
||||
if (extension.location.scheme === REMOTE_HOST_SCHEME) {
|
||||
return remoteExtensionManagementServer;
|
||||
}
|
||||
return null;
|
||||
return webExtensionManagementServer;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,35 +5,31 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { IWorkbenchExtensionEnablementService, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IExtensionService, IExtensionHost } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { AbstractExtensionService } from 'vs/workbench/services/extensions/common/abstractExtensionService';
|
||||
import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/common/extensionHostProcessManager';
|
||||
import { RemoteExtensionHostClient, IInitDataProvider } from 'vs/workbench/services/extensions/common/remoteExtensionHostClient';
|
||||
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
||||
import { AbstractExtensionService, parseScannedExtension } from 'vs/workbench/services/extensions/common/abstractExtensionService';
|
||||
import { RemoteExtensionHost, IRemoteExtensionHostDataProvider, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { WebWorkerExtensionHostStarter } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost';
|
||||
import { canExecuteOnWeb } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { FetchFileSystemProvider } from 'vs/workbench/services/extensions/browser/webWorkerFileSystemProvider';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IStaticExtensionsService } from 'vs/workbench/services/extensions/common/staticExtensions';
|
||||
import { DeltaExtensionsResult } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry';
|
||||
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
|
||||
export class ExtensionService extends AbstractExtensionService implements IExtensionService {
|
||||
|
||||
private _disposables = new DisposableStore();
|
||||
private _remoteExtensionsEnvironmentData: IRemoteAgentEnvironment | null = null;
|
||||
private _remoteInitData: IRemoteExtensionHostInitData | null = null;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@@ -46,7 +42,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
@IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
|
||||
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
|
||||
@IConfigurationService private readonly _configService: IConfigurationService,
|
||||
@IStaticExtensionsService private readonly _staticExtensions: IStaticExtensionsService,
|
||||
@IWebExtensionsScannerService private readonly _webExtensionsScannerService: IWebExtensionsScannerService,
|
||||
) {
|
||||
super(
|
||||
instantiationService,
|
||||
@@ -73,32 +69,39 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
this._disposables.add(this._fileService.registerProvider(Schemas.https, provider));
|
||||
}
|
||||
|
||||
private _createProvider(remoteAuthority: string): IInitDataProvider {
|
||||
private _createLocalExtensionHostDataProvider() {
|
||||
return {
|
||||
remoteAuthority: remoteAuthority,
|
||||
getInitData: async () => {
|
||||
await this.whenInstalledExtensionsRegistered();
|
||||
const connectionData = this._remoteAuthorityResolverService.getConnectionData(remoteAuthority);
|
||||
const remoteEnvironment = this._remoteExtensionsEnvironmentData!;
|
||||
return { connectionData, remoteEnvironment };
|
||||
const allExtensions = await this.getExtensions();
|
||||
const webExtensions = allExtensions.filter(ext => canExecuteOnWeb(ext, this._productService, this._configService));
|
||||
return {
|
||||
autoStart: true,
|
||||
extensions: webExtensions
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected _createExtensionHosts(_isInitialStart: boolean, initialActivationEvents: string[]): ExtensionHostProcessManager[] {
|
||||
const result: ExtensionHostProcessManager[] = [];
|
||||
private _createRemoteExtensionHostDataProvider(remoteAuthority: string): IRemoteExtensionHostDataProvider {
|
||||
return {
|
||||
remoteAuthority: remoteAuthority,
|
||||
getInitData: async () => {
|
||||
await this.whenInstalledExtensionsRegistered();
|
||||
return this._remoteInitData!;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const webExtensions = this.getExtensions().then(extensions => extensions.filter(ext => canExecuteOnWeb(ext, this._productService, this._configService)));
|
||||
const webHostProcessWorker = this._instantiationService.createInstance(WebWorkerExtensionHostStarter, true, webExtensions, URI.file(this._environmentService.logsPath).with({ scheme: this._environmentService.logFile.scheme }));
|
||||
const webHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, webHostProcessWorker, null, initialActivationEvents);
|
||||
result.push(webHostProcessManager);
|
||||
protected _createExtensionHosts(_isInitialStart: boolean): IExtensionHost[] {
|
||||
const result: IExtensionHost[] = [];
|
||||
|
||||
const webWorkerExtHost = this._instantiationService.createInstance(WebWorkerExtensionHost, this._createLocalExtensionHostDataProvider());
|
||||
result.push(webWorkerExtHost);
|
||||
|
||||
const remoteAgentConnection = this._remoteAgentService.getConnection();
|
||||
if (remoteAgentConnection) {
|
||||
const remoteExtensions = this.getExtensions().then(extensions => extensions.filter(ext => !canExecuteOnWeb(ext, this._productService, this._configService)));
|
||||
const remoteExtHostProcessWorker = this._instantiationService.createInstance(RemoteExtensionHostClient, remoteExtensions, this._createProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory);
|
||||
const remoteExtHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, remoteExtHostProcessWorker, remoteAgentConnection.remoteAuthority, initialActivationEvents);
|
||||
result.push(remoteExtHostProcessManager);
|
||||
const remoteExtHost = this._instantiationService.createInstance(RemoteExtensionHost, this._createRemoteExtensionHostDataProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory);
|
||||
result.push(remoteExtHost);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -108,16 +111,18 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
// fetch the remote environment
|
||||
let [remoteEnv, localExtensions] = await Promise.all([
|
||||
this._remoteAgentService.getEnvironment(),
|
||||
this._staticExtensions.getExtensions()
|
||||
this._webExtensionsScannerService.scanExtensions().then(extensions => extensions.map(parseScannedExtension))
|
||||
]);
|
||||
|
||||
const remoteAgentConnection = this._remoteAgentService.getConnection();
|
||||
|
||||
let result: DeltaExtensionsResult;
|
||||
|
||||
// local: only enabled and web'ish extension
|
||||
localExtensions = localExtensions!.filter(ext => this._isEnabled(ext) && canExecuteOnWeb(ext, this._productService, this._configService));
|
||||
this._checkEnableProposedApi(localExtensions);
|
||||
|
||||
if (!remoteEnv) {
|
||||
if (!remoteEnv || !remoteAgentConnection) {
|
||||
result = this._registry.deltaExtensions(localExtensions, []);
|
||||
|
||||
} else {
|
||||
@@ -131,7 +136,17 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
localExtensions = localExtensions.filter(extension => !isRemoteExtension.has(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
|
||||
// save for remote extension's init data
|
||||
this._remoteExtensionsEnvironmentData = remoteEnv;
|
||||
this._remoteInitData = {
|
||||
connectionData: this._remoteAuthorityResolverService.getConnectionData(remoteAgentConnection.remoteAuthority),
|
||||
pid: remoteEnv.pid,
|
||||
appRoot: remoteEnv.appRoot,
|
||||
appSettingsHome: remoteEnv.appSettingsHome,
|
||||
extensionHostLogsPath: remoteEnv.extensionHostLogsPath,
|
||||
globalStorageHome: remoteEnv.globalStorageHome,
|
||||
userHome: remoteEnv.userHome,
|
||||
extensions: remoteEnv.extensions,
|
||||
allExtensions: remoteEnv.extensions.concat(localExtensions)
|
||||
};
|
||||
|
||||
result = this._registry.deltaExtensions(remoteEnv.extensions.concat(localExtensions), []);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtensionHostStarter, ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
@@ -25,7 +25,19 @@ import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export class WebWorkerExtensionHostStarter implements IExtensionHostStarter {
|
||||
export interface IWebWorkerExtensionHostInitData {
|
||||
readonly autoStart: boolean;
|
||||
readonly extensions: IExtensionDescription[];
|
||||
}
|
||||
|
||||
export interface IWebWorkerExtensionHostDataProvider {
|
||||
getInitData(): Promise<IWebWorkerExtensionHostInitData>;
|
||||
}
|
||||
|
||||
export class WebWorkerExtensionHost implements IExtensionHost {
|
||||
|
||||
public readonly kind = ExtensionHostKind.LocalWebWorker;
|
||||
public readonly remoteAuthority = null;
|
||||
|
||||
private _toDispose = new DisposableStore();
|
||||
private _isTerminating: boolean = false;
|
||||
@@ -34,12 +46,11 @@ export class WebWorkerExtensionHostStarter implements IExtensionHostStarter {
|
||||
private readonly _onDidExit = new Emitter<[number, string | null]>();
|
||||
readonly onExit: Event<[number, string | null]> = this._onDidExit.event;
|
||||
|
||||
private readonly _extensionHostLogsLocation: URI;
|
||||
private readonly _extensionHostLogFile: URI;
|
||||
|
||||
constructor(
|
||||
private readonly _autoStart: boolean,
|
||||
private readonly _extensions: Promise<IExtensionDescription[]>,
|
||||
private readonly _extensionHostLogsLocation: URI,
|
||||
private readonly _initDataProvider: IWebWorkerExtensionHostDataProvider,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
|
||||
@ILabelService private readonly _labelService: ILabelService,
|
||||
@@ -47,6 +58,7 @@ export class WebWorkerExtensionHostStarter implements IExtensionHostStarter {
|
||||
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
) {
|
||||
this._extensionHostLogsLocation = URI.file(this._environmentService.logsPath).with({ scheme: this._environmentService.logFile.scheme });
|
||||
this._extensionHostLogFile = joinPath(this._extensionHostLogsLocation, `${ExtensionHostLogFileName}.log`);
|
||||
}
|
||||
|
||||
@@ -127,7 +139,7 @@ export class WebWorkerExtensionHostStarter implements IExtensionHostStarter {
|
||||
}
|
||||
|
||||
private async _createExtHostInitData(): Promise<IInitData> {
|
||||
const [telemetryInfo, extensionDescriptions] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._extensions]);
|
||||
const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]);
|
||||
const workspace = this._contextService.getWorkspace();
|
||||
return {
|
||||
commit: this._productService.commit,
|
||||
@@ -153,12 +165,12 @@ export class WebWorkerExtensionHostStarter implements IExtensionHostStarter {
|
||||
},
|
||||
resolvedExtensions: [],
|
||||
hostExtensions: [],
|
||||
extensions: extensionDescriptions,
|
||||
extensions: initData.extensions,
|
||||
telemetryInfo,
|
||||
logLevel: this._logService.getLevel(),
|
||||
logsLocation: this._extensionHostLogsLocation,
|
||||
logFile: this._extensionHostLogFile,
|
||||
autoStart: this._autoStart,
|
||||
autoStart: initData.autoStart,
|
||||
remote: {
|
||||
authority: this._environmentService.configuration.remoteAuthority,
|
||||
connectionData: null,
|
||||
@@ -15,12 +15,12 @@ import { BetterMergeId } from 'vs/platform/extensionManagement/common/extensionM
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension, IExtensionHost } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry';
|
||||
import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol';
|
||||
import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/common/extensionHostProcessManager';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager';
|
||||
import { ExtensionIdentifier, IExtensionDescription, IScannedExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
@@ -29,6 +29,16 @@ import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtens
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
|
||||
|
||||
export function parseScannedExtension(extension: IScannedExtension): IExtensionDescription {
|
||||
return {
|
||||
identifier: new ExtensionIdentifier(`${extension.packageJSON.publisher}.${extension.packageJSON.name}`),
|
||||
isBuiltin: extension.type === ExtensionType.System,
|
||||
isUnderDevelopment: false,
|
||||
extensionLocation: extension.location,
|
||||
...extension.packageJSON,
|
||||
};
|
||||
}
|
||||
|
||||
export abstract class AbstractExtensionService extends Disposable implements IExtensionService {
|
||||
|
||||
public _serviceBrand: undefined;
|
||||
@@ -58,9 +68,9 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
protected readonly _isExtensionDevTestFromCli: boolean;
|
||||
|
||||
// --- Members used per extension host process
|
||||
protected _extensionHostProcessManagers: ExtensionHostProcessManager[];
|
||||
protected _extensionHostManagers: ExtensionHostManager[];
|
||||
protected _extensionHostActiveExtensions: Map<string, ExtensionIdentifier>;
|
||||
private _extensionHostProcessActivationTimes: Map<string, ActivationTimes>;
|
||||
private _extensionHostActivationTimes: Map<string, ActivationTimes>;
|
||||
private _extensionHostExtensionRuntimeErrors: Map<string, Error[]>;
|
||||
|
||||
constructor(
|
||||
@@ -85,9 +95,9 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
this._extensionsMessages = new Map<string, IMessage[]>();
|
||||
this._proposedApiController = new ProposedApiController(this._environmentService, this._productService);
|
||||
|
||||
this._extensionHostProcessManagers = [];
|
||||
this._extensionHostManagers = [];
|
||||
this._extensionHostActiveExtensions = new Map<string, ExtensionIdentifier>();
|
||||
this._extensionHostProcessActivationTimes = new Map<string, ActivationTimes>();
|
||||
this._extensionHostActivationTimes = new Map<string, ActivationTimes>();
|
||||
this._extensionHostExtensionRuntimeErrors = new Map<string, Error[]>();
|
||||
|
||||
const devOpts = parseExtensionDevOptions(this._environmentService);
|
||||
@@ -97,7 +107,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
|
||||
protected async _initialize(): Promise<void> {
|
||||
perf.mark('willLoadExtensions');
|
||||
this._startExtensionHostProcess(true, []);
|
||||
this._startExtensionHosts(true, []);
|
||||
this.whenInstalledExtensionsRegistered().then(() => perf.mark('didLoadExtensions'));
|
||||
await this._scanAndHandleExtensions();
|
||||
this._releaseBarrier();
|
||||
@@ -110,18 +120,18 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
this._onDidChangeExtensionsStatus.fire(this._registry.getAllExtensionDescriptions().map(e => e.identifier));
|
||||
}
|
||||
|
||||
private _stopExtensionHostProcess(): void {
|
||||
private _stopExtensionHosts(): void {
|
||||
let previouslyActivatedExtensionIds: ExtensionIdentifier[] = [];
|
||||
this._extensionHostActiveExtensions.forEach((value) => {
|
||||
previouslyActivatedExtensionIds.push(value);
|
||||
});
|
||||
|
||||
for (const manager of this._extensionHostProcessManagers) {
|
||||
for (const manager of this._extensionHostManagers) {
|
||||
manager.dispose();
|
||||
}
|
||||
this._extensionHostProcessManagers = [];
|
||||
this._extensionHostManagers = [];
|
||||
this._extensionHostActiveExtensions = new Map<string, ExtensionIdentifier>();
|
||||
this._extensionHostProcessActivationTimes = new Map<string, ActivationTimes>();
|
||||
this._extensionHostActivationTimes = new Map<string, ActivationTimes>();
|
||||
this._extensionHostExtensionRuntimeErrors = new Map<string, Error[]>();
|
||||
|
||||
if (previouslyActivatedExtensionIds.length > 0) {
|
||||
@@ -129,18 +139,19 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
}
|
||||
}
|
||||
|
||||
private _startExtensionHostProcess(isInitialStart: boolean, initialActivationEvents: string[]): void {
|
||||
this._stopExtensionHostProcess();
|
||||
private _startExtensionHosts(isInitialStart: boolean, initialActivationEvents: string[]): void {
|
||||
this._stopExtensionHosts();
|
||||
|
||||
const processManagers = this._createExtensionHosts(isInitialStart, initialActivationEvents);
|
||||
processManagers.forEach((processManager) => {
|
||||
const extensionHosts = this._createExtensionHosts(isInitialStart);
|
||||
extensionHosts.forEach((extensionHost) => {
|
||||
const processManager = this._instantiationService.createInstance(ExtensionHostManager, extensionHost, initialActivationEvents);
|
||||
processManager.onDidExit(([code, signal]) => this._onExtensionHostCrashOrExit(processManager, code, signal));
|
||||
processManager.onDidChangeResponsiveState((responsiveState) => { this._onDidChangeResponsiveChange.fire({ isResponsive: responsiveState === ResponsiveState.Responsive }); });
|
||||
this._extensionHostProcessManagers.push(processManager);
|
||||
this._extensionHostManagers.push(processManager);
|
||||
});
|
||||
}
|
||||
|
||||
private _onExtensionHostCrashOrExit(extensionHost: ExtensionHostProcessManager, code: number, signal: string | null): void {
|
||||
private _onExtensionHostCrashOrExit(extensionHost: ExtensionHostManager, code: number, signal: string | null): void {
|
||||
|
||||
// Unexpected termination
|
||||
if (!this._isExtensionDevHost) {
|
||||
@@ -151,9 +162,9 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
this._onExtensionHostExit(code);
|
||||
}
|
||||
|
||||
protected _onExtensionHostCrashed(extensionHost: ExtensionHostProcessManager, code: number, signal: string | null): void {
|
||||
protected _onExtensionHostCrashed(extensionHost: ExtensionHostManager, code: number, signal: string | null): void {
|
||||
console.error('Extension host terminated unexpectedly. Code: ', code, ' Signal: ', signal);
|
||||
this._stopExtensionHostProcess();
|
||||
this._stopExtensionHosts();
|
||||
}
|
||||
|
||||
//#region IExtensionService
|
||||
@@ -167,12 +178,12 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
}
|
||||
|
||||
public restartExtensionHost(): void {
|
||||
this._stopExtensionHostProcess();
|
||||
this._startExtensionHostProcess(false, Array.from(this._allRequestedActivateEvents.keys()));
|
||||
this._stopExtensionHosts();
|
||||
this._startExtensionHosts(false, Array.from(this._allRequestedActivateEvents.keys()));
|
||||
}
|
||||
|
||||
protected startExtensionHost(): void {
|
||||
this._startExtensionHostProcess(false, Array.from(this._allRequestedActivateEvents.keys()));
|
||||
this._startExtensionHosts(false, Array.from(this._allRequestedActivateEvents.keys()));
|
||||
}
|
||||
|
||||
public activateByEvent(activationEvent: string): Promise<void> {
|
||||
@@ -200,7 +211,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
|
||||
private _activateByEvent(activationEvent: string): Promise<void> {
|
||||
const result = Promise.all(
|
||||
this._extensionHostProcessManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent))
|
||||
this._extensionHostManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent))
|
||||
).then(() => { });
|
||||
this._onWillActivateByEvent.fire({
|
||||
event: activationEvent,
|
||||
@@ -248,7 +259,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
const extensionKey = ExtensionIdentifier.toKey(extension.identifier);
|
||||
result[extension.identifier.value] = {
|
||||
messages: this._extensionsMessages.get(extensionKey) || [],
|
||||
activationTimes: this._extensionHostProcessActivationTimes.get(extensionKey),
|
||||
activationTimes: this._extensionHostActivationTimes.get(extensionKey),
|
||||
runtimeErrors: this._extensionHostExtensionRuntimeErrors.get(extensionKey) || [],
|
||||
};
|
||||
}
|
||||
@@ -261,7 +272,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
}
|
||||
|
||||
public async setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void> {
|
||||
await this._extensionHostProcessManagers
|
||||
await this._extensionHostManagers
|
||||
.map(manager => manager.setRemoteEnvironment(env));
|
||||
}
|
||||
|
||||
@@ -275,6 +286,14 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
}
|
||||
}
|
||||
|
||||
protected _checkEnabledAndProposedAPI(extensions: IExtensionDescription[]): IExtensionDescription[] {
|
||||
// enable or disable proposed API per extension
|
||||
this._checkEnableProposedApi(extensions);
|
||||
|
||||
// keep only enabled extensions
|
||||
return extensions.filter(extension => this._isEnabled(extension));
|
||||
}
|
||||
|
||||
private _isExtensionUnderDevelopment(extension: IExtensionDescription): boolean {
|
||||
if (this._environmentService.isExtensionDevelopment) {
|
||||
const extDevLocs = this._environmentService.extensionDevelopmentLocationURI;
|
||||
@@ -291,21 +310,17 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
}
|
||||
|
||||
protected _isEnabled(extension: IExtensionDescription): boolean {
|
||||
return !this._isDisabled(extension);
|
||||
}
|
||||
|
||||
protected _isDisabled(extension: IExtensionDescription): boolean {
|
||||
if (this._isExtensionUnderDevelopment(extension)) {
|
||||
// Never disable extensions under development
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ExtensionIdentifier.equals(extension.identifier, BetterMergeId)) {
|
||||
// Check if this is the better merge extension which was migrated to a built-in extension
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return !this._extensionEnablementService.isEnabled(toExtension(extension));
|
||||
return this._extensionEnablementService.isEnabled(toExtension(extension));
|
||||
}
|
||||
|
||||
protected _doHandleExtensionPoints(affectedExtensions: IExtensionDescription[]): void {
|
||||
@@ -413,7 +428,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
|
||||
public async _activateById(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void> {
|
||||
const results = await Promise.all(
|
||||
this._extensionHostProcessManagers.map(manager => manager.activate(extensionId, reason))
|
||||
this._extensionHostManagers.map(manager => manager.activate(extensionId, reason))
|
||||
);
|
||||
const activated = results.some(e => e);
|
||||
if (!activated) {
|
||||
@@ -426,7 +441,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
}
|
||||
|
||||
public _onDidActivateExtension(extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void {
|
||||
this._extensionHostProcessActivationTimes.set(ExtensionIdentifier.toKey(extensionId), new ActivationTimes(codeLoadingTime, activateCallTime, activateResolvedTime, activationReason));
|
||||
this._extensionHostActivationTimes.set(ExtensionIdentifier.toKey(extensionId), new ActivationTimes(codeLoadingTime, activateCallTime, activateResolvedTime, activationReason));
|
||||
this._onDidChangeExtensionsStatus.fire([extensionId]);
|
||||
}
|
||||
|
||||
@@ -441,7 +456,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx
|
||||
|
||||
//#endregion
|
||||
|
||||
protected abstract _createExtensionHosts(isInitialStart: boolean, initialActivationEvents: string[]): ExtensionHostProcessManager[];
|
||||
protected abstract _createExtensionHosts(isInitialStart: boolean): IExtensionHost[];
|
||||
protected abstract _scanAndHandleExtensions(): Promise<void>;
|
||||
public abstract _onExtensionHostExit(code: number): void;
|
||||
}
|
||||
|
||||
@@ -21,17 +21,17 @@ import { registerAction2, Action2 } from 'vs/platform/actions/common/actions';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { IExtensionHostStarter } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IExtensionHost, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator';
|
||||
|
||||
// Enable to see detailed message communication between window and extension host
|
||||
const LOG_EXTENSION_HOST_COMMUNICATION = false;
|
||||
const LOG_USE_COLORS = true;
|
||||
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
|
||||
|
||||
export class ExtensionHostProcessManager extends Disposable {
|
||||
export class ExtensionHostManager extends Disposable {
|
||||
|
||||
public readonly kind: ExtensionHostKind;
|
||||
public readonly onDidExit: Event<[number, string | null]>;
|
||||
|
||||
private readonly _onDidChangeResponsiveState: Emitter<ResponsiveState> = this._register(new Emitter<ResponsiveState>());
|
||||
@@ -40,32 +40,31 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
/**
|
||||
* A map of already activated events to speed things up if the same activation event is triggered multiple times.
|
||||
*/
|
||||
private readonly _extensionHostProcessFinishedActivateEvents: { [activationEvent: string]: boolean; };
|
||||
private _extensionHostProcessRPCProtocol: RPCProtocol | null;
|
||||
private readonly _extensionHostProcessCustomers: IDisposable[];
|
||||
private readonly _extensionHostProcessWorker: IExtensionHostStarter;
|
||||
private readonly _finishedActivateEvents: { [activationEvent: string]: boolean; };
|
||||
private _rpcProtocol: RPCProtocol | null;
|
||||
private readonly _customers: IDisposable[];
|
||||
private readonly _extensionHost: IExtensionHost;
|
||||
/**
|
||||
* winjs believes a proxy is a promise because it has a `then` method, so wrap the result in an object.
|
||||
*/
|
||||
private _extensionHostProcessProxy: Promise<{ value: ExtHostExtensionServiceShape; } | null> | null;
|
||||
private _proxy: Promise<{ value: ExtHostExtensionServiceShape; } | null> | null;
|
||||
private _resolveAuthorityAttempt: number;
|
||||
|
||||
constructor(
|
||||
public readonly isLocal: boolean,
|
||||
extensionHostProcessWorker: IExtensionHostStarter,
|
||||
private readonly _remoteAuthority: string | null,
|
||||
extensionHost: IExtensionHost,
|
||||
initialActivationEvents: string[],
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
|
||||
) {
|
||||
super();
|
||||
this._extensionHostProcessFinishedActivateEvents = Object.create(null);
|
||||
this._extensionHostProcessRPCProtocol = null;
|
||||
this._extensionHostProcessCustomers = [];
|
||||
this._finishedActivateEvents = Object.create(null);
|
||||
this._rpcProtocol = null;
|
||||
this._customers = [];
|
||||
|
||||
this._extensionHostProcessWorker = extensionHostProcessWorker;
|
||||
this.onDidExit = this._extensionHostProcessWorker.onExit;
|
||||
this._extensionHostProcessProxy = this._extensionHostProcessWorker.start()!.then(
|
||||
this._extensionHost = extensionHost;
|
||||
this.kind = this._extensionHost.kind;
|
||||
this.onDidExit = this._extensionHost.onExit;
|
||||
this._proxy = this._extensionHost.start()!.then(
|
||||
(protocol) => {
|
||||
return { value: this._createExtensionHostCustomers(protocol) };
|
||||
},
|
||||
@@ -75,7 +74,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
return null;
|
||||
}
|
||||
);
|
||||
this._extensionHostProcessProxy.then(() => {
|
||||
this._proxy.then(() => {
|
||||
initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent));
|
||||
this._register(registerLatencyTestProvider({
|
||||
measure: () => this.measure()
|
||||
@@ -85,27 +84,27 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._extensionHostProcessWorker) {
|
||||
this._extensionHostProcessWorker.dispose();
|
||||
if (this._extensionHost) {
|
||||
this._extensionHost.dispose();
|
||||
}
|
||||
if (this._extensionHostProcessRPCProtocol) {
|
||||
this._extensionHostProcessRPCProtocol.dispose();
|
||||
if (this._rpcProtocol) {
|
||||
this._rpcProtocol.dispose();
|
||||
}
|
||||
for (let i = 0, len = this._extensionHostProcessCustomers.length; i < len; i++) {
|
||||
const customer = this._extensionHostProcessCustomers[i];
|
||||
for (let i = 0, len = this._customers.length; i < len; i++) {
|
||||
const customer = this._customers[i];
|
||||
try {
|
||||
customer.dispose();
|
||||
} catch (err) {
|
||||
errors.onUnexpectedError(err);
|
||||
}
|
||||
}
|
||||
this._extensionHostProcessProxy = null;
|
||||
this._proxy = null;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private async measure(): Promise<ExtHostLatencyResult | null> {
|
||||
const proxy = await this._getExtensionHostProcessProxy();
|
||||
const proxy = await this._getProxy();
|
||||
if (!proxy) {
|
||||
return null;
|
||||
}
|
||||
@@ -113,18 +112,18 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
const down = await this._measureDown(proxy);
|
||||
const up = await this._measureUp(proxy);
|
||||
return {
|
||||
remoteAuthority: this._remoteAuthority,
|
||||
remoteAuthority: this._extensionHost.remoteAuthority,
|
||||
latency,
|
||||
down,
|
||||
up
|
||||
};
|
||||
}
|
||||
|
||||
private async _getExtensionHostProcessProxy(): Promise<ExtHostExtensionServiceShape | null> {
|
||||
if (!this._extensionHostProcessProxy) {
|
||||
private async _getProxy(): Promise<ExtHostExtensionServiceShape | null> {
|
||||
if (!this._proxy) {
|
||||
return null;
|
||||
}
|
||||
const p = await this._extensionHostProcessProxy;
|
||||
const p = await this._proxy;
|
||||
if (!p) {
|
||||
return null;
|
||||
}
|
||||
@@ -159,7 +158,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
const sw = StopWatch.create(true);
|
||||
await proxy.$test_up(buff);
|
||||
sw.stop();
|
||||
return ExtensionHostProcessManager._convert(SIZE, sw.elapsed());
|
||||
return ExtensionHostManager._convert(SIZE, sw.elapsed());
|
||||
}
|
||||
|
||||
private async _measureDown(proxy: ExtHostExtensionServiceShape): Promise<number> {
|
||||
@@ -168,7 +167,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
const sw = StopWatch.create(true);
|
||||
await proxy.$test_down(SIZE);
|
||||
sw.stop();
|
||||
return ExtensionHostProcessManager._convert(SIZE, sw.elapsed());
|
||||
return ExtensionHostManager._convert(SIZE, sw.elapsed());
|
||||
}
|
||||
|
||||
private _createExtensionHostCustomers(protocol: IMessagePassingProtocol): ExtHostExtensionServiceShape {
|
||||
@@ -178,13 +177,13 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
logger = new RPCLogger();
|
||||
}
|
||||
|
||||
this._extensionHostProcessRPCProtocol = new RPCProtocol(protocol, logger);
|
||||
this._register(this._extensionHostProcessRPCProtocol.onDidChangeResponsiveState((responsiveState: ResponsiveState) => this._onDidChangeResponsiveState.fire(responsiveState)));
|
||||
this._rpcProtocol = new RPCProtocol(protocol, logger);
|
||||
this._register(this._rpcProtocol.onDidChangeResponsiveState((responsiveState: ResponsiveState) => this._onDidChangeResponsiveState.fire(responsiveState)));
|
||||
const extHostContext: IExtHostContext = {
|
||||
remoteAuthority: this._remoteAuthority! /* TODO: alexdima, remove not-null assertion */,
|
||||
getProxy: <T>(identifier: ProxyIdentifier<T>): T => this._extensionHostProcessRPCProtocol!.getProxy(identifier),
|
||||
set: <T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R => this._extensionHostProcessRPCProtocol!.set(identifier, instance),
|
||||
assertRegistered: (identifiers: ProxyIdentifier<any>[]): void => this._extensionHostProcessRPCProtocol!.assertRegistered(identifiers),
|
||||
remoteAuthority: this._extensionHost.remoteAuthority,
|
||||
getProxy: <T>(identifier: ProxyIdentifier<T>): T => this._rpcProtocol!.getProxy(identifier),
|
||||
set: <T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R => this._rpcProtocol!.set(identifier, instance),
|
||||
assertRegistered: (identifiers: ProxyIdentifier<any>[]): void => this._rpcProtocol!.assertRegistered(identifiers),
|
||||
};
|
||||
|
||||
// Named customers
|
||||
@@ -192,28 +191,28 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
for (let i = 0, len = namedCustomers.length; i < len; i++) {
|
||||
const [id, ctor] = namedCustomers[i];
|
||||
const instance = this._instantiationService.createInstance(ctor, extHostContext);
|
||||
this._extensionHostProcessCustomers.push(instance);
|
||||
this._extensionHostProcessRPCProtocol.set(id, instance);
|
||||
this._customers.push(instance);
|
||||
this._rpcProtocol.set(id, instance);
|
||||
}
|
||||
|
||||
// Customers
|
||||
const customers = ExtHostCustomersRegistry.getCustomers();
|
||||
for (const ctor of customers) {
|
||||
const instance = this._instantiationService.createInstance(ctor, extHostContext);
|
||||
this._extensionHostProcessCustomers.push(instance);
|
||||
this._customers.push(instance);
|
||||
}
|
||||
|
||||
// Check that no named customers are missing
|
||||
// {{SQL CARBON EDIT}} filter out services we don't expose
|
||||
const filtered: ProxyIdentifier<any>[] = [MainContext.MainThreadDebugService, MainContext.MainThreadTask];
|
||||
const expected: ProxyIdentifier<any>[] = Object.keys(MainContext).map((key) => (<any>MainContext)[key]).filter(v => !filtered.some(x => x === v));
|
||||
this._extensionHostProcessRPCProtocol.assertRegistered(expected);
|
||||
this._rpcProtocol.assertRegistered(expected);
|
||||
|
||||
return this._extensionHostProcessRPCProtocol.getProxy(ExtHostContext.ExtHostExtensionService);
|
||||
return this._rpcProtocol.getProxy(ExtHostContext.ExtHostExtensionService);
|
||||
}
|
||||
|
||||
public async activate(extension: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<boolean> {
|
||||
const proxy = await this._getExtensionHostProcessProxy();
|
||||
const proxy = await this._getProxy();
|
||||
if (!proxy) {
|
||||
return false;
|
||||
}
|
||||
@@ -221,10 +220,10 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
}
|
||||
|
||||
public activateByEvent(activationEvent: string): Promise<void> {
|
||||
if (this._extensionHostProcessFinishedActivateEvents[activationEvent] || !this._extensionHostProcessProxy) {
|
||||
if (this._finishedActivateEvents[activationEvent] || !this._proxy) {
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
return this._extensionHostProcessProxy.then((proxy) => {
|
||||
return this._proxy.then((proxy) => {
|
||||
if (!proxy) {
|
||||
// this case is already covered above and logged.
|
||||
// i.e. the extension host could not be started
|
||||
@@ -232,16 +231,16 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
}
|
||||
return proxy.value.$activateByEvent(activationEvent);
|
||||
}).then(() => {
|
||||
this._extensionHostProcessFinishedActivateEvents[activationEvent] = true;
|
||||
this._finishedActivateEvents[activationEvent] = true;
|
||||
});
|
||||
}
|
||||
|
||||
public async getInspectPort(tryEnableInspector: boolean): Promise<number> {
|
||||
if (this._extensionHostProcessWorker) {
|
||||
if (this._extensionHost) {
|
||||
if (tryEnableInspector) {
|
||||
await this._extensionHostProcessWorker.enableInspectPort();
|
||||
await this._extensionHost.enableInspectPort();
|
||||
}
|
||||
let port = this._extensionHostProcessWorker.getInspectPort();
|
||||
let port = this._extensionHost.getInspectPort();
|
||||
if (port) {
|
||||
return port;
|
||||
}
|
||||
@@ -262,7 +261,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
}
|
||||
});
|
||||
}
|
||||
const proxy = await this._getExtensionHostProcessProxy();
|
||||
const proxy = await this._getProxy();
|
||||
if (!proxy) {
|
||||
throw new Error(`Cannot resolve authority`);
|
||||
}
|
||||
@@ -276,7 +275,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
}
|
||||
|
||||
public async start(enabledExtensionIds: ExtensionIdentifier[]): Promise<void> {
|
||||
const proxy = await this._getExtensionHostProcessProxy();
|
||||
const proxy = await this._getProxy();
|
||||
if (!proxy) {
|
||||
return;
|
||||
}
|
||||
@@ -284,7 +283,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
}
|
||||
|
||||
public async deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void> {
|
||||
const proxy = await this._getExtensionHostProcessProxy();
|
||||
const proxy = await this._getProxy();
|
||||
if (!proxy) {
|
||||
return;
|
||||
}
|
||||
@@ -292,7 +291,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
}
|
||||
|
||||
public async setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void> {
|
||||
const proxy = await this._getExtensionHostProcessProxy();
|
||||
const proxy = await this._getProxy();
|
||||
if (!proxy) {
|
||||
return;
|
||||
}
|
||||
@@ -396,7 +395,7 @@ registerAction2(class MeasureExtHostLatencyAction extends Action2 {
|
||||
value: nls.localize('measureExtHostLatency', "Measure Extension Host Latency"),
|
||||
original: 'Measure Extension Host Latency'
|
||||
},
|
||||
category: nls.localize('developer', "Developer"),
|
||||
category: nls.localize({ key: 'developer', comment: ['A developer on Code itself or someone diagnosing issues in Code'] }, "Developer"),
|
||||
f1: true
|
||||
});
|
||||
}
|
||||
@@ -24,6 +24,8 @@ export const nullExtensionDescription = Object.freeze(<IExtensionDescription>{
|
||||
isBuiltin: false,
|
||||
});
|
||||
|
||||
export const webWorkerExtHostConfig = 'extensions.webWorker';
|
||||
|
||||
export const IExtensionService = createDecorator<IExtensionService>('extensionService');
|
||||
|
||||
export interface IMessage {
|
||||
@@ -84,7 +86,15 @@ export interface IExtensionHostProfile {
|
||||
getAggregatedTimes(): Map<ProfileSegmentId, number>;
|
||||
}
|
||||
|
||||
export interface IExtensionHostStarter {
|
||||
export const enum ExtensionHostKind {
|
||||
LocalProcess,
|
||||
LocalWebWorker,
|
||||
Remote
|
||||
}
|
||||
|
||||
export interface IExtensionHost {
|
||||
readonly kind: ExtensionHostKind;
|
||||
readonly remoteAuthority: string | null;
|
||||
readonly onExit: Event<[number, string | null]>;
|
||||
|
||||
start(): Promise<IMessagePassingProtocol> | null;
|
||||
@@ -257,6 +267,17 @@ export function toExtension(extensionDescription: IExtensionDescription): IExten
|
||||
};
|
||||
}
|
||||
|
||||
export function toExtensionDescription(extension: IExtension): IExtensionDescription {
|
||||
return {
|
||||
identifier: new ExtensionIdentifier(extension.identifier.id),
|
||||
isBuiltin: extension.type === ExtensionType.System,
|
||||
isUnderDevelopment: false,
|
||||
extensionLocation: extension.location,
|
||||
...extension.manifest,
|
||||
uuid: extension.identifier.uuid
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export class NullExtensionService implements IExtensionService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/co
|
||||
import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IMessage } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { ExtensionIdentifier, IExtensionDescription, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions';
|
||||
import { values } from 'vs/base/common/map';
|
||||
|
||||
const schemaRegistry = Registry.as<IJSONContributionRegistry>(Extensions.JSONContribution);
|
||||
@@ -187,7 +187,7 @@ export const schema: IJSONSchema = {
|
||||
items: {
|
||||
oneOf: [{
|
||||
type: 'string',
|
||||
enum: ['Programming Languages', 'Snippets', 'Linters', 'Themes', 'Debuggers', 'Other', 'Keymaps', 'Formatters', 'Extension Packs', 'SCM Providers', 'Azure', 'Language Packs'],
|
||||
enum: EXTENSION_CATEGORIES,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
@@ -271,7 +271,7 @@ export const schema: IJSONSchema = {
|
||||
},
|
||||
{
|
||||
label: 'onStartupFinished',
|
||||
description: nls.localize('vscode.extension.activationEvents.onStartupFinished', 'An activation event emitted after the start-up finished (after all eager activated extensions have finished activating).'),
|
||||
description: nls.localize('vscode.extension.activationEvents.onStartupFinished', 'An activation event emitted after the start-up finished (after all `*` activated extensions have finished activating).'),
|
||||
body: 'onStartupFinished'
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,6 +20,11 @@ export function prefersExecuteOnWorkspace(manifest: IExtensionManifest, productS
|
||||
return (extensionKind.length > 0 && extensionKind[0] === 'workspace');
|
||||
}
|
||||
|
||||
export function prefersExecuteOnWeb(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): boolean {
|
||||
const extensionKind = getExtensionKind(manifest, productService, configurationService);
|
||||
return (extensionKind.length > 0 && extensionKind[0] === 'web');
|
||||
}
|
||||
|
||||
export function canExecuteOnUI(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): boolean {
|
||||
const extensionKind = getExtensionKind(manifest, productService, configurationService);
|
||||
return extensionKind.some(kind => kind === 'ui');
|
||||
|
||||
@@ -13,9 +13,8 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IInitData, UIKind } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
|
||||
import { IExtensionHostStarter, ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions';
|
||||
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
||||
import { IRemoteAuthorityResolverService, IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
@@ -33,30 +32,37 @@ import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export interface IRemoteInitData {
|
||||
export interface IRemoteExtensionHostInitData {
|
||||
readonly connectionData: IRemoteConnectionData | null;
|
||||
readonly remoteEnvironment: IRemoteAgentEnvironment;
|
||||
readonly pid: number;
|
||||
readonly appRoot: URI;
|
||||
readonly appSettingsHome: URI;
|
||||
readonly extensionHostLogsPath: URI;
|
||||
readonly globalStorageHome: URI;
|
||||
readonly userHome: URI;
|
||||
readonly extensions: IExtensionDescription[];
|
||||
readonly allExtensions: IExtensionDescription[];
|
||||
}
|
||||
|
||||
export interface IInitDataProvider {
|
||||
export interface IRemoteExtensionHostDataProvider {
|
||||
readonly remoteAuthority: string;
|
||||
getInitData(): Promise<IRemoteInitData>;
|
||||
getInitData(): Promise<IRemoteExtensionHostInitData>;
|
||||
}
|
||||
|
||||
export class RemoteExtensionHostClient extends Disposable implements IExtensionHostStarter {
|
||||
export class RemoteExtensionHost extends Disposable implements IExtensionHost {
|
||||
|
||||
public readonly kind = ExtensionHostKind.Remote;
|
||||
public readonly remoteAuthority: string;
|
||||
|
||||
private _onExit: Emitter<[number, string | null]> = this._register(new Emitter<[number, string | null]>());
|
||||
public readonly onExit: Event<[number, string | null]> = this._onExit.event;
|
||||
|
||||
private _protocol: PersistentProtocol | null;
|
||||
|
||||
private _terminating: boolean;
|
||||
private readonly _isExtensionDevHost: boolean;
|
||||
|
||||
private _terminating: boolean;
|
||||
|
||||
constructor(
|
||||
private readonly _allExtensions: Promise<IExtensionDescription[]>,
|
||||
private readonly _initDataProvider: IInitDataProvider,
|
||||
private readonly _initDataProvider: IRemoteExtensionHostDataProvider,
|
||||
private readonly _socketFactory: ISocketFactory,
|
||||
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
|
||||
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
|
||||
@@ -70,6 +76,7 @@ export class RemoteExtensionHostClient extends Disposable implements IExtensionH
|
||||
@ISignService private readonly _signService: ISignService
|
||||
) {
|
||||
super();
|
||||
this.remoteAuthority = this._initDataProvider.remoteAuthority;
|
||||
this._protocol = null;
|
||||
this._terminating = false;
|
||||
|
||||
@@ -194,54 +201,52 @@ export class RemoteExtensionHostClient extends Disposable implements IExtensionH
|
||||
this._onExit.fire([0, null]);
|
||||
}
|
||||
|
||||
private _createExtHostInitData(isExtensionDevelopmentDebug: boolean): Promise<IInitData> {
|
||||
return Promise.all([this._allExtensions, this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]).then(([allExtensions, telemetryInfo, remoteInitData]) => {
|
||||
// Collect all identifiers for extension ids which can be considered "resolved"
|
||||
const resolvedExtensions = allExtensions.filter(extension => !extension.main).map(extension => extension.identifier);
|
||||
const hostExtensions = allExtensions.filter(extension => extension.main && extension.api === 'none').map(extension => extension.identifier);
|
||||
const workspace = this._contextService.getWorkspace();
|
||||
const remoteEnv = remoteInitData.remoteEnvironment;
|
||||
const r: IInitData = {
|
||||
commit: this._productService.commit,
|
||||
version: this._productService.version,
|
||||
vscodeVersion: this._productService.vscodeVersion, // {{SQL CARBON EDIT}} add vscode version
|
||||
parentPid: remoteEnv.pid,
|
||||
environment: {
|
||||
isExtensionDevelopmentDebug,
|
||||
appRoot: remoteEnv.appRoot,
|
||||
appSettingsHome: remoteEnv.appSettingsHome,
|
||||
appName: this._productService.nameLong,
|
||||
appUriScheme: this._productService.urlProtocol,
|
||||
appLanguage: platform.language,
|
||||
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
|
||||
extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,
|
||||
globalStorageHome: remoteEnv.globalStorageHome,
|
||||
userHome: remoteEnv.userHome,
|
||||
webviewResourceRoot: this._environmentService.webviewResourceRoot,
|
||||
webviewCspSource: this._environmentService.webviewCspSource,
|
||||
},
|
||||
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : {
|
||||
configuration: workspace.configuration,
|
||||
id: workspace.id,
|
||||
name: this._labelService.getWorkspaceLabel(workspace)
|
||||
},
|
||||
remote: {
|
||||
isRemote: true,
|
||||
authority: this._initDataProvider.remoteAuthority,
|
||||
connectionData: remoteInitData.connectionData
|
||||
},
|
||||
resolvedExtensions: resolvedExtensions,
|
||||
hostExtensions: hostExtensions,
|
||||
extensions: remoteEnv.extensions,
|
||||
telemetryInfo,
|
||||
logLevel: this._logService.getLevel(),
|
||||
logsLocation: remoteEnv.extensionHostLogsPath,
|
||||
logFile: joinPath(remoteEnv.extensionHostLogsPath, `${ExtensionHostLogFileName}.log`),
|
||||
autoStart: true,
|
||||
uiKind: platform.isWeb ? UIKind.Web : UIKind.Desktop
|
||||
};
|
||||
return r;
|
||||
});
|
||||
private async _createExtHostInitData(isExtensionDevelopmentDebug: boolean): Promise<IInitData> {
|
||||
const [telemetryInfo, remoteInitData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]);
|
||||
|
||||
// Collect all identifiers for extension ids which can be considered "resolved"
|
||||
const resolvedExtensions = remoteInitData.allExtensions.filter(extension => !extension.main).map(extension => extension.identifier);
|
||||
const hostExtensions = remoteInitData.allExtensions.filter(extension => extension.main && extension.api === 'none').map(extension => extension.identifier);
|
||||
const workspace = this._contextService.getWorkspace();
|
||||
return {
|
||||
commit: this._productService.commit,
|
||||
version: this._productService.version,
|
||||
vscodeVersion: this._productService.vscodeVersion, // {{SQL CARBON EDIT}} add vscode version
|
||||
parentPid: remoteInitData.pid,
|
||||
environment: {
|
||||
isExtensionDevelopmentDebug,
|
||||
appRoot: remoteInitData.appRoot,
|
||||
appSettingsHome: remoteInitData.appSettingsHome,
|
||||
appName: this._productService.nameLong,
|
||||
appUriScheme: this._productService.urlProtocol,
|
||||
appLanguage: platform.language,
|
||||
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
|
||||
extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,
|
||||
globalStorageHome: remoteInitData.globalStorageHome,
|
||||
userHome: remoteInitData.userHome,
|
||||
webviewResourceRoot: this._environmentService.webviewResourceRoot,
|
||||
webviewCspSource: this._environmentService.webviewCspSource,
|
||||
},
|
||||
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : {
|
||||
configuration: workspace.configuration,
|
||||
id: workspace.id,
|
||||
name: this._labelService.getWorkspaceLabel(workspace)
|
||||
},
|
||||
remote: {
|
||||
isRemote: true,
|
||||
authority: this._initDataProvider.remoteAuthority,
|
||||
connectionData: remoteInitData.connectionData
|
||||
},
|
||||
resolvedExtensions: resolvedExtensions,
|
||||
hostExtensions: hostExtensions,
|
||||
extensions: remoteInitData.extensions,
|
||||
telemetryInfo,
|
||||
logLevel: this._logService.getLevel(),
|
||||
logsLocation: remoteInitData.extensionHostLogsPath,
|
||||
logFile: joinPath(remoteInitData.extensionHostLogsPath, `${ExtensionHostLogFileName}.log`),
|
||||
autoStart: true,
|
||||
uiKind: platform.isWeb ? UIKind.Web : UIKind.Desktop
|
||||
};
|
||||
}
|
||||
|
||||
getInspectPort(): number | undefined {
|
||||
@@ -1,40 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IExtensionDescription, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
|
||||
export const IStaticExtensionsService = createDecorator<IStaticExtensionsService>('IStaticExtensionsService');
|
||||
|
||||
export interface IStaticExtensionsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
getExtensions(): Promise<IExtensionDescription[]>;
|
||||
}
|
||||
|
||||
export class StaticExtensionsService implements IStaticExtensionsService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _descriptions: IExtensionDescription[] = [];
|
||||
|
||||
constructor(@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService) {
|
||||
const staticExtensions = environmentService.options && Array.isArray(environmentService.options.staticExtensions) ? environmentService.options.staticExtensions : [];
|
||||
|
||||
this._descriptions = staticExtensions.map(data => <IExtensionDescription>{
|
||||
identifier: new ExtensionIdentifier(`${data.packageJSON.publisher}.${data.packageJSON.name}`),
|
||||
extensionLocation: data.extensionLocation,
|
||||
isBuiltin: !!data.isBuiltin,
|
||||
...data.packageJSON,
|
||||
});
|
||||
}
|
||||
|
||||
async getExtensions(): Promise<IExtensionDescription[]> {
|
||||
return this._descriptions;
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IStaticExtensionsService, StaticExtensionsService, true);
|
||||
@@ -3,28 +3,27 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ExtensionHostProcessWorker } from 'vs/workbench/services/extensions/electron-browser/extensionHost';
|
||||
import { LocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-browser/localProcessExtensionHost';
|
||||
import { CachedExtensionScanner } from 'vs/workbench/services/extensions/electron-browser/cachedExtensionScanner';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { AbstractExtensionService } from 'vs/workbench/services/extensions/common/abstractExtensionService';
|
||||
import { AbstractExtensionService, parseScannedExtension } from 'vs/workbench/services/extensions/common/abstractExtensionService';
|
||||
import * as nls from 'vs/nls';
|
||||
import { runWhenIdle } from 'vs/base/common/async';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { IWorkbenchExtensionEnablementService, EnablementState, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IInitDataProvider, RemoteExtensionHostClient } from 'vs/workbench/services/extensions/common/remoteExtensionHostClient';
|
||||
import { IRemoteExtensionHostDataProvider, RemoteExtensionHost, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil';
|
||||
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/common/extensionHostProcessManager';
|
||||
import { IExtensionService, toExtension, ExtensionHostKind, IExtensionHost, webWorkerExtHostConfig } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager';
|
||||
import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
@@ -32,7 +31,6 @@ import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteA
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { Logger } from 'vs/workbench/services/extensions/common/extensionPoints';
|
||||
import { flatten } from 'vs/base/common/arrays';
|
||||
import { IStaticExtensionsService } from 'vs/workbench/services/extensions/common/staticExtensions';
|
||||
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
|
||||
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
|
||||
import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService';
|
||||
@@ -41,6 +39,8 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
|
||||
import { getRemoteName } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
||||
import { WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost';
|
||||
|
||||
class DeltaExtensionsQueueItem {
|
||||
constructor(
|
||||
@@ -51,8 +51,9 @@ class DeltaExtensionsQueueItem {
|
||||
|
||||
export class ExtensionService extends AbstractExtensionService implements IExtensionService {
|
||||
|
||||
private readonly _remoteEnvironment: Map<string, IRemoteAgentEnvironment>;
|
||||
|
||||
private readonly _enableLocalWebWorker: boolean;
|
||||
private readonly _remoteInitData: Map<string, IRemoteExtensionHostInitData>;
|
||||
private _runningLocation: Map<string, ExtensionRunningLocation>;
|
||||
private readonly _extensionScanner: CachedExtensionScanner;
|
||||
private _deltaExtensionsQueue: DeltaExtensionsQueueItem[];
|
||||
|
||||
@@ -69,7 +70,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
@IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
|
||||
@IStaticExtensionsService private readonly _staticExtensions: IStaticExtensionsService,
|
||||
@IWebExtensionsScannerService private readonly _webExtensionsScannerService: IWebExtensionsScannerService,
|
||||
@IElectronService private readonly _electronService: IElectronService,
|
||||
@IHostService private readonly _hostService: IHostService,
|
||||
@IRemoteExplorerService private readonly _remoteExplorerService: IRemoteExplorerService,
|
||||
@@ -85,16 +86,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
productService
|
||||
);
|
||||
|
||||
if (this._extensionEnablementService.allUserExtensionsDisabled) {
|
||||
this._notificationService.prompt(Severity.Info, nls.localize('extensionsDisabled', "All installed extensions are temporarily disabled. Reload the window to return to the previous state."), [{
|
||||
label: nls.localize('Reload', "Reload"),
|
||||
run: () => {
|
||||
this._hostService.reload();
|
||||
}
|
||||
}]);
|
||||
}
|
||||
this._enableLocalWebWorker = this._configurationService.getValue<boolean>(webWorkerExtHostConfig);
|
||||
|
||||
this._remoteEnvironment = new Map<string, IRemoteAgentEnvironment>();
|
||||
this._remoteInitData = new Map<string, IRemoteExtensionHostInitData>();
|
||||
this._runningLocation = new Map<string, ExtensionRunningLocation>();
|
||||
|
||||
this._extensionScanner = instantiationService.createInstance(CachedExtensionScanner);
|
||||
this._deltaExtensionsQueue = [];
|
||||
@@ -142,8 +137,28 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
this._initialize();
|
||||
}, 50 /*max delay*/);
|
||||
});
|
||||
|
||||
// delay notification for extensions disabled until workbench restored
|
||||
if (this._extensionEnablementService.allUserExtensionsDisabled) {
|
||||
this._lifecycleService.when(LifecyclePhase.Restored).then(() => {
|
||||
this._notificationService.prompt(Severity.Info, nls.localize('extensionsDisabled', "All installed extensions are temporarily disabled. Reload the window to return to the previous state."), [{
|
||||
label: nls.localize('Reload', "Reload"),
|
||||
run: () => {
|
||||
this._hostService.reload();
|
||||
}
|
||||
}]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _getExtensionHostManager(kind: ExtensionHostKind): ExtensionHostManager | null {
|
||||
for (const extensionHostManager of this._extensionHostManagers) {
|
||||
if (extensionHostManager.kind === kind) {
|
||||
return extensionHostManager;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//#region deltaExtensions
|
||||
|
||||
@@ -222,11 +237,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
this._checkEnableProposedApi(toAdd);
|
||||
|
||||
// Update extension points
|
||||
this._rehandleExtensionPoints((<IExtensionDescription[]>[]).concat(toAdd).concat(toRemove));
|
||||
this._doHandleExtensionPoints((<IExtensionDescription[]>[]).concat(toAdd).concat(toRemove));
|
||||
|
||||
// Update the extension host
|
||||
if (this._extensionHostProcessManagers.length > 0) {
|
||||
await this._extensionHostProcessManagers[0].deltaExtensions(toAdd, toRemove.map(e => e.identifier));
|
||||
const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess);
|
||||
if (localProcessExtensionHost) {
|
||||
await localProcessExtensionHost.deltaExtensions(toAdd, toRemove.map(e => e.identifier));
|
||||
}
|
||||
|
||||
for (let i = 0; i < toAdd.length; i++) {
|
||||
@@ -234,15 +250,11 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
}
|
||||
}
|
||||
|
||||
private _rehandleExtensionPoints(extensionDescriptions: IExtensionDescription[]): void {
|
||||
this._doHandleExtensionPoints(extensionDescriptions);
|
||||
}
|
||||
|
||||
public canAddExtension(extensionDescription: IExtensionDescription): boolean {
|
||||
return this._canAddExtension(toExtension(extensionDescription));
|
||||
}
|
||||
|
||||
public _canAddExtension(extension: IExtension): boolean {
|
||||
private _canAddExtension(extension: IExtension): boolean {
|
||||
if (this._environmentService.configuration.remoteAuthority) {
|
||||
return false;
|
||||
}
|
||||
@@ -338,57 +350,78 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
|
||||
if (shouldActivate) {
|
||||
await Promise.all(
|
||||
this._extensionHostProcessManagers.map(extHostManager => extHostManager.activate(extensionDescription.identifier, { startup: false, extensionId: extensionDescription.identifier, activationEvent: shouldActivateReason! }))
|
||||
this._extensionHostManagers.map(extHostManager => extHostManager.activate(extensionDescription.identifier, { startup: false, extensionId: extensionDescription.identifier, activationEvent: shouldActivateReason! }))
|
||||
).then(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
private _createProvider(remoteAuthority: string): IInitDataProvider {
|
||||
private async _scanAllLocalExtensions(): Promise<IExtensionDescription[]> {
|
||||
return flatten(await Promise.all([
|
||||
this._extensionScanner.scannedExtensions,
|
||||
this._webExtensionsScannerService.scanExtensions().then(extensions => extensions.map(parseScannedExtension))
|
||||
]));
|
||||
}
|
||||
|
||||
private _createLocalExtensionHostDataProvider(isInitialStart: boolean, desiredRunningLocation: ExtensionRunningLocation) {
|
||||
return {
|
||||
remoteAuthority: remoteAuthority,
|
||||
getInitData: async () => {
|
||||
await this.whenInstalledExtensionsRegistered();
|
||||
const connectionData = this._remoteAuthorityResolverService.getConnectionData(remoteAuthority);
|
||||
const remoteEnvironment = this._remoteEnvironment.get(remoteAuthority)!;
|
||||
return { connectionData, remoteEnvironment };
|
||||
if (isInitialStart) {
|
||||
const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions());
|
||||
const runningLocation = determineRunningLocation(this._productService, this._configurationService, localExtensions, [], false, this._enableLocalWebWorker);
|
||||
const localProcessExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation);
|
||||
return {
|
||||
autoStart: false,
|
||||
extensions: localProcessExtensions
|
||||
};
|
||||
} else {
|
||||
// restart case
|
||||
const allExtensions = await this.getExtensions();
|
||||
const localProcessExtensions = filterByRunningLocation(allExtensions, this._runningLocation, desiredRunningLocation);
|
||||
return {
|
||||
autoStart: true,
|
||||
extensions: localProcessExtensions
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected _createExtensionHosts(isInitialStart: boolean, initialActivationEvents: string[]): ExtensionHostProcessManager[] {
|
||||
let autoStart: boolean;
|
||||
let extensions: Promise<IExtensionDescription[]>;
|
||||
if (isInitialStart) {
|
||||
autoStart = false;
|
||||
extensions = this._extensionScanner.scannedExtensions.then(extensions => extensions.filter(extension => this._isEnabled(extension))); // remove disabled extensions
|
||||
} else {
|
||||
// restart case
|
||||
autoStart = true;
|
||||
extensions = this.getExtensions().then((extensions) => extensions.filter(ext => ext.extensionLocation.scheme === Schemas.file));
|
||||
private _createRemoteExtensionHostDataProvider(remoteAuthority: string): IRemoteExtensionHostDataProvider {
|
||||
return {
|
||||
remoteAuthority: remoteAuthority,
|
||||
getInitData: async () => {
|
||||
await this.whenInstalledExtensionsRegistered();
|
||||
return this._remoteInitData.get(remoteAuthority)!;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected _createExtensionHosts(isInitialStart: boolean): IExtensionHost[] {
|
||||
const result: IExtensionHost[] = [];
|
||||
|
||||
const localProcessExtHost = this._instantiationService.createInstance(LocalProcessExtensionHost, this._createLocalExtensionHostDataProvider(isInitialStart, ExtensionRunningLocation.LocalProcess));
|
||||
result.push(localProcessExtHost);
|
||||
|
||||
if (this._enableLocalWebWorker) {
|
||||
const webWorkerExtHost = this._instantiationService.createInstance(WebWorkerExtensionHost, this._createLocalExtensionHostDataProvider(isInitialStart, ExtensionRunningLocation.LocalWebWorker));
|
||||
result.push(webWorkerExtHost);
|
||||
}
|
||||
|
||||
const result: ExtensionHostProcessManager[] = [];
|
||||
|
||||
const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, autoStart, extensions, this._environmentService.extHostLogsPath);
|
||||
const extHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, true, extHostProcessWorker, null, initialActivationEvents);
|
||||
result.push(extHostProcessManager);
|
||||
|
||||
const remoteAgentConnection = this._remoteAgentService.getConnection();
|
||||
if (remoteAgentConnection) {
|
||||
const remoteExtHostProcessWorker = this._instantiationService.createInstance(RemoteExtensionHostClient, this.getExtensions(), this._createProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory);
|
||||
const remoteExtHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, remoteExtHostProcessWorker, remoteAgentConnection.remoteAuthority, initialActivationEvents);
|
||||
result.push(remoteExtHostProcessManager);
|
||||
const remoteExtHost = this._instantiationService.createInstance(RemoteExtensionHost, this._createRemoteExtensionHostDataProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory);
|
||||
result.push(remoteExtHost);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected _onExtensionHostCrashed(extensionHost: ExtensionHostProcessManager, code: number, signal: string | null): void {
|
||||
protected _onExtensionHostCrashed(extensionHost: ExtensionHostManager, code: number, signal: string | null): void {
|
||||
super._onExtensionHostCrashed(extensionHost, code, signal);
|
||||
|
||||
if (extensionHost.isLocal) {
|
||||
if (extensionHost.kind === ExtensionHostKind.LocalProcess) {
|
||||
if (code === 55) {
|
||||
this._notificationService.prompt(
|
||||
Severity.Error,
|
||||
@@ -437,10 +470,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
return;
|
||||
}
|
||||
|
||||
const extensionHost = this._extensionHostProcessManagers[0];
|
||||
const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!;
|
||||
this._remoteAuthorityResolverService._clearResolvedAuthority(remoteAuthority);
|
||||
try {
|
||||
const result = await extensionHost.resolveAuthority(remoteAuthority);
|
||||
const result = await localProcessExtensionHost.resolveAuthority(remoteAuthority);
|
||||
this._remoteAuthorityResolverService._setResolvedAuthority(result.authority, result.options);
|
||||
} catch (err) {
|
||||
this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err);
|
||||
@@ -451,25 +484,19 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
this._extensionScanner.startScanningExtensions(this.createLogger());
|
||||
|
||||
const remoteAuthority = this._environmentService.configuration.remoteAuthority;
|
||||
const extensionHost = this._extensionHostProcessManagers[0];
|
||||
const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!;
|
||||
|
||||
const allExtensions = flatten(await Promise.all([this._extensionScanner.scannedExtensions, this._staticExtensions.getExtensions()]));
|
||||
|
||||
// enable or disable proposed API per extension
|
||||
this._checkEnableProposedApi(allExtensions);
|
||||
|
||||
// remove disabled extensions
|
||||
let localExtensions = remove(allExtensions, extension => this._isDisabled(extension));
|
||||
let localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions());
|
||||
let remoteEnv: IRemoteAgentEnvironment | null = null;
|
||||
|
||||
if (remoteAuthority) {
|
||||
let resolvedAuthority: ResolverResult;
|
||||
let resolverResult: ResolverResult;
|
||||
|
||||
try {
|
||||
resolvedAuthority = await extensionHost.resolveAuthority(remoteAuthority);
|
||||
resolverResult = await localProcessExtensionHost.resolveAuthority(remoteAuthority);
|
||||
} catch (err) {
|
||||
const remoteName = getRemoteName(remoteAuthority);
|
||||
if (RemoteAuthorityResolverError.isNoResolverFound(err)) {
|
||||
err.isHandled = await this._handleNoResolverFound(remoteName, allExtensions);
|
||||
err.isHandled = await this._handleNoResolverFound(remoteAuthority);
|
||||
} else {
|
||||
console.log(err);
|
||||
if (RemoteAuthorityResolverError.isHandled(err)) {
|
||||
@@ -479,22 +506,18 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
this._remoteAuthorityResolverService._setResolvedAuthorityError(remoteAuthority, err);
|
||||
|
||||
// Proceed with the local extension host
|
||||
await this._startLocalExtensionHost(extensionHost, localExtensions, localExtensions.map(extension => extension.identifier));
|
||||
await this._startLocalExtensionHost(localExtensions);
|
||||
return;
|
||||
}
|
||||
|
||||
// set the resolved authority
|
||||
this._remoteAuthorityResolverService._setResolvedAuthority(resolvedAuthority.authority, resolvedAuthority.options);
|
||||
this._remoteExplorerService.setTunnelInformation(resolvedAuthority.tunnelInformation);
|
||||
this._remoteAuthorityResolverService._setResolvedAuthority(resolverResult.authority, resolverResult.options);
|
||||
this._remoteExplorerService.setTunnelInformation(resolverResult.tunnelInformation);
|
||||
|
||||
// monitor for breakage
|
||||
const connection = this._remoteAgentService.getConnection();
|
||||
if (connection) {
|
||||
connection.onDidStateChange(async (e) => {
|
||||
const remoteAuthority = this._environmentService.configuration.remoteAuthority;
|
||||
if (!remoteAuthority) {
|
||||
return;
|
||||
}
|
||||
if (e.type === PersistentConnectionEventType.ConnectionLost) {
|
||||
this._remoteAuthorityResolverService._clearResolvedAuthority(remoteAuthority);
|
||||
}
|
||||
@@ -503,80 +526,64 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
}
|
||||
|
||||
// fetch the remote environment
|
||||
const remoteEnv = (await this._remoteAgentService.getEnvironment());
|
||||
remoteEnv = await this._remoteAgentService.getEnvironment();
|
||||
|
||||
if (!remoteEnv) {
|
||||
this._notificationService.notify({ severity: Severity.Error, message: nls.localize('getEnvironmentFailure', "Could not fetch remote environment") });
|
||||
// Proceed with the local extension host
|
||||
await this._startLocalExtensionHost(extensionHost, localExtensions, localExtensions.map(extension => extension.identifier));
|
||||
await this._startLocalExtensionHost(localExtensions);
|
||||
return;
|
||||
}
|
||||
|
||||
// enable or disable proposed API per extension
|
||||
this._checkEnableProposedApi(remoteEnv.extensions);
|
||||
|
||||
// remove disabled extensions
|
||||
remoteEnv.extensions = remove(remoteEnv.extensions, extension => this._isDisabled(extension));
|
||||
|
||||
// Determine where each extension will execute, based on extensionKind
|
||||
const isInstalledLocally = new Set<string>();
|
||||
localExtensions.forEach(ext => isInstalledLocally.add(ExtensionIdentifier.toKey(ext.identifier)));
|
||||
|
||||
const isInstalledRemotely = new Set<string>();
|
||||
remoteEnv.extensions.forEach(ext => isInstalledRemotely.add(ExtensionIdentifier.toKey(ext.identifier)));
|
||||
|
||||
const enum RunningLocation { None, Local, Remote }
|
||||
const pickRunningLocation = (extension: IExtensionDescription): RunningLocation => {
|
||||
for (const extensionKind of getExtensionKind(extension, this._productService, this._configurationService)) {
|
||||
if (extensionKind === 'ui') {
|
||||
if (isInstalledLocally.has(ExtensionIdentifier.toKey(extension.identifier))) {
|
||||
return RunningLocation.Local;
|
||||
}
|
||||
} else if (extensionKind === 'workspace') {
|
||||
if (isInstalledRemotely.has(ExtensionIdentifier.toKey(extension.identifier))) {
|
||||
return RunningLocation.Remote;
|
||||
}
|
||||
}
|
||||
}
|
||||
return RunningLocation.None;
|
||||
};
|
||||
|
||||
const runningLocation = new Map<string, RunningLocation>();
|
||||
localExtensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext)));
|
||||
remoteEnv.extensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext)));
|
||||
|
||||
// remove non-UI extensions from the local extensions
|
||||
localExtensions = localExtensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === RunningLocation.Local);
|
||||
|
||||
// in case of UI extensions overlap, the local extension wins
|
||||
remoteEnv.extensions = remoteEnv.extensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === RunningLocation.Remote);
|
||||
|
||||
// save for remote extension's init data
|
||||
this._remoteEnvironment.set(remoteAuthority, remoteEnv);
|
||||
|
||||
await this._startLocalExtensionHost(extensionHost, remoteEnv.extensions.concat(localExtensions), localExtensions.map(extension => extension.identifier));
|
||||
} else {
|
||||
await this._startLocalExtensionHost(extensionHost, localExtensions, localExtensions.map(extension => extension.identifier));
|
||||
}
|
||||
|
||||
await this._startLocalExtensionHost(localExtensions, remoteAuthority, remoteEnv);
|
||||
}
|
||||
|
||||
private async _startLocalExtensionHost(extensionHost: ExtensionHostProcessManager, allExtensions: IExtensionDescription[], localExtensions: ExtensionIdentifier[]): Promise<void> {
|
||||
this._registerAndHandleExtensions(allExtensions);
|
||||
extensionHost.start(localExtensions.filter(id => this._registry.containsExtension(id)));
|
||||
}
|
||||
private async _startLocalExtensionHost(localExtensions: IExtensionDescription[], remoteAuthority: string | undefined = undefined, remoteEnv: IRemoteAgentEnvironment | null = null): Promise<void> {
|
||||
|
||||
private _registerAndHandleExtensions(allExtensions: IExtensionDescription[]): void {
|
||||
const result = this._registry.deltaExtensions(allExtensions, []);
|
||||
let remoteExtensions = remoteEnv ? this._checkEnabledAndProposedAPI(remoteEnv.extensions) : [];
|
||||
|
||||
this._runningLocation = determineRunningLocation(this._productService, this._configurationService, localExtensions, remoteExtensions, Boolean(remoteAuthority), this._enableLocalWebWorker);
|
||||
|
||||
// remove non-UI extensions from the local extensions
|
||||
const localProcessExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalProcess);
|
||||
const localWebWorkerExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker);
|
||||
remoteExtensions = filterByRunningLocation(remoteExtensions, this._runningLocation, ExtensionRunningLocation.Remote);
|
||||
|
||||
const result = this._registry.deltaExtensions(remoteExtensions.concat(localProcessExtensions).concat(localWebWorkerExtensions), []);
|
||||
if (result.removedDueToLooping.length > 0) {
|
||||
this._logOrShowMessage(Severity.Error, nls.localize('looping', "The following extensions contain dependency loops and have been disabled: {0}", result.removedDueToLooping.map(e => `'${e.identifier.value}'`).join(', ')));
|
||||
}
|
||||
|
||||
if (remoteAuthority && remoteEnv) {
|
||||
this._remoteInitData.set(remoteAuthority, {
|
||||
connectionData: this._remoteAuthorityResolverService.getConnectionData(remoteAuthority),
|
||||
pid: remoteEnv.pid,
|
||||
appRoot: remoteEnv.appRoot,
|
||||
appSettingsHome: remoteEnv.appSettingsHome,
|
||||
extensionHostLogsPath: remoteEnv.extensionHostLogsPath,
|
||||
globalStorageHome: remoteEnv.globalStorageHome,
|
||||
userHome: remoteEnv.userHome,
|
||||
extensions: remoteExtensions,
|
||||
allExtensions: this._registry.getAllExtensionDescriptions(),
|
||||
});
|
||||
}
|
||||
|
||||
this._doHandleExtensionPoints(this._registry.getAllExtensionDescriptions());
|
||||
|
||||
const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!;
|
||||
localProcessExtensionHost.start(localProcessExtensions.map(extension => extension.identifier).filter(id => this._registry.containsExtension(id)));
|
||||
|
||||
const localWebWorkerExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalWebWorker);
|
||||
if (localWebWorkerExtensionHost) {
|
||||
localWebWorkerExtensionHost.start(localWebWorkerExtensions.map(extension => extension.identifier).filter(id => this._registry.containsExtension(id)));
|
||||
}
|
||||
}
|
||||
|
||||
public async getInspectPort(tryEnableInspector: boolean): Promise<number> {
|
||||
if (this._extensionHostProcessManagers.length > 0) {
|
||||
return this._extensionHostProcessManagers[0].getInspectPort(tryEnableInspector);
|
||||
const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess);
|
||||
if (localProcessExtensionHost) {
|
||||
return localProcessExtensionHost.getInspectPort(tryEnableInspector);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -591,7 +598,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleNoResolverFound(remoteName: string, allExtensions: IExtensionDescription[]): Promise<boolean> {
|
||||
private async _handleNoResolverFound(remoteAuthority: string): Promise<boolean> {
|
||||
const remoteName = getRemoteName(remoteAuthority);
|
||||
const recommendation = this._productService.remoteExtensionTips?.[remoteName];
|
||||
if (!recommendation) {
|
||||
return false;
|
||||
@@ -607,9 +615,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
};
|
||||
|
||||
const resolverExtensionId = recommendation.extensionId;
|
||||
const allExtensions = await this._scanAllLocalExtensions();
|
||||
const extension = allExtensions.filter(e => e.identifier.value === resolverExtensionId)[0];
|
||||
if (extension) {
|
||||
if (this._isDisabled(extension)) {
|
||||
if (!this._isEnabled(extension)) {
|
||||
const message = nls.localize('enableResolver', "Extension '{0}' is required to open the remote window.\nOK to enable?", recommendation.friendlyName);
|
||||
this._notificationService.prompt(Severity.Info, message,
|
||||
[{
|
||||
@@ -649,27 +658,59 @@ export class ExtensionService extends AbstractExtensionService implements IExten
|
||||
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function remove(arr: IExtensionDescription[], predicate: (item: IExtensionDescription) => boolean): IExtensionDescription[];
|
||||
function remove(arr: IExtensionDescription[], toRemove: IExtensionDescription[]): IExtensionDescription[];
|
||||
function remove(arr: IExtensionDescription[], arg2: ((item: IExtensionDescription) => boolean) | IExtensionDescription[]): IExtensionDescription[] {
|
||||
if (typeof arg2 === 'function') {
|
||||
return _removePredicate(arr, arg2);
|
||||
}
|
||||
return _removeSet(arr, arg2);
|
||||
const enum ExtensionRunningLocation {
|
||||
None,
|
||||
LocalProcess,
|
||||
LocalWebWorker,
|
||||
Remote
|
||||
}
|
||||
|
||||
function _removePredicate(arr: IExtensionDescription[], predicate: (item: IExtensionDescription) => boolean): IExtensionDescription[] {
|
||||
return arr.filter(extension => !predicate(extension));
|
||||
function determineRunningLocation(productService: IProductService, configurationService: IConfigurationService, localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], hasRemote: boolean, hasLocalWebWorker: boolean): Map<string, ExtensionRunningLocation> {
|
||||
const localExtensionsSet = new Set<string>();
|
||||
localExtensions.forEach(ext => localExtensionsSet.add(ExtensionIdentifier.toKey(ext.identifier)));
|
||||
|
||||
const remoteExtensionsSet = new Set<string>();
|
||||
remoteExtensions.forEach(ext => remoteExtensionsSet.add(ExtensionIdentifier.toKey(ext.identifier)));
|
||||
|
||||
const pickRunningLocation = (extension: IExtensionDescription): ExtensionRunningLocation => {
|
||||
const isInstalledLocally = localExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier));
|
||||
const isInstalledRemotely = remoteExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier));
|
||||
for (const extensionKind of getExtensionKind(extension, productService, configurationService)) {
|
||||
if (extensionKind === 'ui' && isInstalledLocally) {
|
||||
// ui extensions run locally if possible
|
||||
return ExtensionRunningLocation.LocalProcess;
|
||||
}
|
||||
if (extensionKind === 'workspace' && isInstalledRemotely) {
|
||||
// workspace extensions run remotely if possible
|
||||
return ExtensionRunningLocation.Remote;
|
||||
}
|
||||
if (extensionKind === 'workspace' && !hasRemote) {
|
||||
// workspace extensions also run locally if there is no remote
|
||||
return ExtensionRunningLocation.LocalProcess;
|
||||
}
|
||||
if (extensionKind === 'web' && isInstalledLocally && hasLocalWebWorker) {
|
||||
// web worker extensions run in the local web worker if possible
|
||||
if (typeof extension.browser !== 'undefined') {
|
||||
// The "browser" field determines the entry point
|
||||
(<any>extension).main = extension.browser;
|
||||
}
|
||||
return ExtensionRunningLocation.LocalWebWorker;
|
||||
}
|
||||
}
|
||||
return ExtensionRunningLocation.None;
|
||||
};
|
||||
|
||||
const runningLocation = new Map<string, ExtensionRunningLocation>();
|
||||
localExtensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext)));
|
||||
remoteExtensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext)));
|
||||
return runningLocation;
|
||||
}
|
||||
|
||||
function _removeSet(arr: IExtensionDescription[], toRemove: IExtensionDescription[]): IExtensionDescription[] {
|
||||
const toRemoveSet = new Set<string>();
|
||||
toRemove.forEach(extension => toRemoveSet.add(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
return arr.filter(extension => !toRemoveSet.has(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
function filterByRunningLocation(extensions: IExtensionDescription[], runningLocation: Map<string, ExtensionRunningLocation>, desiredRunningLocation: ExtensionRunningLocation): IExtensionDescription[] {
|
||||
return extensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === desiredRunningLocation);
|
||||
}
|
||||
|
||||
registerSingleton(IExtensionService, ExtensionService);
|
||||
@@ -693,4 +734,4 @@ class RestartExtensionHostAction extends Action {
|
||||
}
|
||||
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(SyncActionDescriptor.from(RestartExtensionHostAction), 'Developer: Restart Extension Host', nls.localize('developer', "Developer"));
|
||||
registry.registerWorkbenchAction(SyncActionDescriptor.from(RestartExtensionHostAction), 'Developer: Restart Extension Host', nls.localize({ key: 'developer', comment: ['A developer on Code itself or someone diagnosing issues in Code'] }, "Developer"));
|
||||
|
||||
@@ -37,7 +37,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'
|
||||
import { parseExtensionDevOptions } from '../common/extensionDevOptions';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug';
|
||||
import { IExtensionHostStarter, ExtensionHostLogFileName } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
@@ -45,7 +45,19 @@ import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output';
|
||||
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
|
||||
|
||||
export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
export interface ILocalProcessExtensionHostInitData {
|
||||
readonly autoStart: boolean;
|
||||
readonly extensions: IExtensionDescription[];
|
||||
}
|
||||
|
||||
export interface ILocalProcessExtensionHostDataProvider {
|
||||
getInitData(): Promise<ILocalProcessExtensionHostInitData>;
|
||||
}
|
||||
|
||||
export class LocalProcessExtensionHost implements IExtensionHost {
|
||||
|
||||
public readonly kind = ExtensionHostKind.LocalProcess;
|
||||
public readonly remoteAuthority = null;
|
||||
|
||||
private readonly _onExit: Emitter<[number, string]> = new Emitter<[number, string]>();
|
||||
public readonly onExit: Event<[number, string]> = this._onExit.event;
|
||||
@@ -73,9 +85,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
private readonly _extensionHostLogFile: URI;
|
||||
|
||||
constructor(
|
||||
private readonly _autoStart: boolean,
|
||||
private readonly _extensions: Promise<IExtensionDescription[]>,
|
||||
private readonly _extensionHostLogsLocation: URI,
|
||||
private readonly _initDataProvider: ILocalProcessExtensionHostDataProvider,
|
||||
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IElectronService private readonly _electronService: IElectronService,
|
||||
@@ -103,7 +113,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
this._extensionHostConnection = null;
|
||||
this._messageProtocol = null;
|
||||
|
||||
this._extensionHostLogFile = joinPath(this._extensionHostLogsLocation, `${ExtensionHostLogFileName}.log`);
|
||||
this._extensionHostLogFile = joinPath(this._environmentService.extHostLogsPath, `${ExtensionHostLogFileName}.log`);
|
||||
|
||||
this._toDispose.add(this._onExit);
|
||||
this._toDispose.add(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e)));
|
||||
@@ -410,52 +420,49 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
});
|
||||
}
|
||||
|
||||
private _createExtHostInitData(): Promise<IInitData> {
|
||||
return Promise.all([this._telemetryService.getTelemetryInfo(), this._extensions])
|
||||
.then(([telemetryInfo, extensionDescriptions]) => {
|
||||
const workspace = this._contextService.getWorkspace();
|
||||
const r: IInitData = {
|
||||
commit: this._productService.commit,
|
||||
version: this._productService.version,
|
||||
vscodeVersion: this._productService.vscodeVersion, // {{SQL CARBON EDIT}} add vscode version
|
||||
parentPid: process.pid,
|
||||
environment: {
|
||||
isExtensionDevelopmentDebug: this._isExtensionDevDebug,
|
||||
appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined,
|
||||
appSettingsHome: this._environmentService.appSettingsHome ? this._environmentService.appSettingsHome : undefined,
|
||||
appName: this._productService.nameLong,
|
||||
appUriScheme: this._productService.urlProtocol,
|
||||
appLanguage: platform.language,
|
||||
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
|
||||
extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,
|
||||
globalStorageHome: URI.file(this._environmentService.globalStorageHome),
|
||||
userHome: this._environmentService.userHome,
|
||||
webviewResourceRoot: this._environmentService.webviewResourceRoot,
|
||||
webviewCspSource: this._environmentService.webviewCspSource,
|
||||
},
|
||||
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : {
|
||||
configuration: withNullAsUndefined(workspace.configuration),
|
||||
id: workspace.id,
|
||||
name: this._labelService.getWorkspaceLabel(workspace),
|
||||
isUntitled: workspace.configuration ? isUntitledWorkspace(workspace.configuration, this._environmentService) : false
|
||||
},
|
||||
remote: {
|
||||
authority: this._environmentService.configuration.remoteAuthority,
|
||||
connectionData: null,
|
||||
isRemote: false
|
||||
},
|
||||
resolvedExtensions: [],
|
||||
hostExtensions: [],
|
||||
extensions: extensionDescriptions,
|
||||
telemetryInfo,
|
||||
logLevel: this._logService.getLevel(),
|
||||
logsLocation: this._extensionHostLogsLocation,
|
||||
logFile: this._extensionHostLogFile,
|
||||
autoStart: this._autoStart,
|
||||
uiKind: UIKind.Desktop
|
||||
};
|
||||
return r;
|
||||
});
|
||||
private async _createExtHostInitData(): Promise<IInitData> {
|
||||
const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]);
|
||||
const workspace = this._contextService.getWorkspace();
|
||||
return {
|
||||
commit: this._productService.commit,
|
||||
version: this._productService.version,
|
||||
vscodeVersion: this._productService.vscodeVersion, // {{SQL CARBON EDIT}} add vscode version
|
||||
parentPid: process.pid,
|
||||
environment: {
|
||||
isExtensionDevelopmentDebug: this._isExtensionDevDebug,
|
||||
appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined,
|
||||
appSettingsHome: this._environmentService.appSettingsHome ? this._environmentService.appSettingsHome : undefined,
|
||||
appName: this._productService.nameLong,
|
||||
appUriScheme: this._productService.urlProtocol,
|
||||
appLanguage: platform.language,
|
||||
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
|
||||
extensionTestsLocationURI: this._environmentService.extensionTestsLocationURI,
|
||||
globalStorageHome: URI.file(this._environmentService.globalStorageHome),
|
||||
userHome: this._environmentService.userHome,
|
||||
webviewResourceRoot: this._environmentService.webviewResourceRoot,
|
||||
webviewCspSource: this._environmentService.webviewCspSource,
|
||||
},
|
||||
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? undefined : {
|
||||
configuration: withNullAsUndefined(workspace.configuration),
|
||||
id: workspace.id,
|
||||
name: this._labelService.getWorkspaceLabel(workspace),
|
||||
isUntitled: workspace.configuration ? isUntitledWorkspace(workspace.configuration, this._environmentService) : false
|
||||
},
|
||||
remote: {
|
||||
authority: this._environmentService.configuration.remoteAuthority,
|
||||
connectionData: null,
|
||||
isRemote: false
|
||||
},
|
||||
resolvedExtensions: [],
|
||||
hostExtensions: [],
|
||||
extensions: initData.extensions,
|
||||
telemetryInfo,
|
||||
logLevel: this._logService.getLevel(),
|
||||
logsLocation: this._environmentService.extHostLogsPath,
|
||||
logFile: this._extensionHostLogFile,
|
||||
autoStart: initData.autoStart,
|
||||
uiKind: UIKind.Desktop
|
||||
};
|
||||
}
|
||||
|
||||
private _logExtensionHostMessage(entry: IRemoteConsoleLog) {
|
||||
@@ -22,6 +22,7 @@ import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { ExtHostLogService } from 'vs/workbench/api/worker/extHostLogService';
|
||||
import { IExtHostTunnelService, ExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
|
||||
import { IExtHostApiDeprecationService, ExtHostApiDeprecationService, } from 'vs/workbench/api/common/extHostApiDeprecationService';
|
||||
import { IExtHostWindow, ExtHostWindow } from 'vs/workbench/api/common/extHostWindow';
|
||||
import { NotImplementedProxy } from 'vs/base/common/types';
|
||||
|
||||
// register singleton services
|
||||
@@ -29,6 +30,7 @@ registerSingleton(ILogService, ExtHostLogService);
|
||||
registerSingleton(IExtHostApiDeprecationService, ExtHostApiDeprecationService);
|
||||
registerSingleton(IExtHostOutputService, ExtHostOutputService);
|
||||
registerSingleton(IExtHostWorkspace, ExtHostWorkspace);
|
||||
registerSingleton(IExtHostWindow, ExtHostWindow);
|
||||
registerSingleton(IExtHostDecorations, ExtHostDecorations);
|
||||
registerSingleton(IExtHostConfiguration, ExtHostConfiguration);
|
||||
registerSingleton(IExtHostCommands, ExtHostCommands);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ITextEditorOptions, IResourceEditorInput, TextEditorSelectionRevealType
|
||||
import { IEditorInput, IEditorPane, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier, EditorsOrder, SideBySideEditor } from 'vs/workbench/common/editor';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IHistoryService } from 'vs/workbench/services/history/common/history';
|
||||
import { FileChangesEvent, IFileService, FileChangeType } from 'vs/platform/files/common/files';
|
||||
import { FileChangesEvent, IFileService, FileChangeType, FILES_EXCLUDE_CONFIG } from 'vs/platform/files/common/files';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
@@ -19,7 +19,7 @@ import { Event } from 'vs/base/common/event';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { createResourceExcludeMatcher } from 'vs/workbench/services/search/common/search';
|
||||
import { getExcludes, ISearchConfiguration, SEARCH_EXCLUDE_CONFIG } from 'vs/workbench/services/search/common/search';
|
||||
import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
|
||||
@@ -32,6 +32,9 @@ import { addDisposableListener, EventType, EventHelper } from 'vs/base/browser/d
|
||||
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { extUri } from 'vs/base/common/resources';
|
||||
import { IdleValue } from 'vs/base/common/async';
|
||||
import { ResourceGlobMatcher } from 'vs/workbench/common/resources';
|
||||
|
||||
/**
|
||||
* Stores the selection & view state of an editor and allows to compare it to other selection states.
|
||||
@@ -127,7 +130,6 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
this._register(this.editorService.onDidCloseEditor(event => this.onEditorClosed(event)));
|
||||
this._register(this.storageService.onWillSaveState(() => this.saveState()));
|
||||
this._register(this.fileService.onDidFilesChange(event => this.onDidFilesChange(event)));
|
||||
this._register(this.resourceExcludeMatcher.onExpressionChange(() => this.removeExcludedFromHistory()));
|
||||
this._register(this.editorService.onDidMostRecentlyActiveEditorsChange(() => this.handleEditorEventInRecentEditorsStack()));
|
||||
|
||||
// if the service is created late enough that an editor is already opened
|
||||
@@ -577,7 +579,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
const resourceEditorInputA = arg1 as IResourceEditorInput;
|
||||
const resourceEditorInputB = inputB as IResourceEditorInput;
|
||||
|
||||
return resourceEditorInputA && resourceEditorInputB && resourceEditorInputA.resource.toString() === resourceEditorInputB.resource.toString();
|
||||
return resourceEditorInputA && resourceEditorInputB && extUri.isEqual(resourceEditorInputA.resource, resourceEditorInputB.resource);
|
||||
}
|
||||
|
||||
private matchesFile(resource: URI, arg2: IEditorInput | IResourceEditorInput | FileChangesEvent): boolean {
|
||||
@@ -595,12 +597,12 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
return false; // make sure to only check this when workbench has restored (for https://github.com/Microsoft/vscode/issues/48275)
|
||||
}
|
||||
|
||||
return inputResource.toString() === resource.toString();
|
||||
return extUri.isEqual(inputResource, resource);
|
||||
}
|
||||
|
||||
const resourceEditorInput = arg2 as IResourceEditorInput;
|
||||
|
||||
return resourceEditorInput?.resource.toString() === resource.toString();
|
||||
return extUri.isEqual(resourceEditorInput?.resource, resource);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
@@ -632,7 +634,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
if (URI.isUri(editorResource)) {
|
||||
associatedResources.push(editorResource);
|
||||
} else if (editorResource) {
|
||||
associatedResources.push(...coalesce([editorResource.master, editorResource.detail]));
|
||||
associatedResources.push(...coalesce([editorResource.primary, editorResource.secondary]));
|
||||
}
|
||||
|
||||
// Remove from list of recently closed before...
|
||||
@@ -768,7 +770,17 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
|
||||
private history: Array<IEditorInput | IResourceEditorInput> | undefined = undefined;
|
||||
|
||||
private readonly resourceExcludeMatcher = this._register(createResourceExcludeMatcher(this.instantiationService, this.configurationService));
|
||||
private readonly resourceExcludeMatcher = this._register(new IdleValue(() => {
|
||||
const matcher = this._register(this.instantiationService.createInstance(
|
||||
ResourceGlobMatcher,
|
||||
root => getExcludes(root ? this.configurationService.getValue<ISearchConfiguration>({ resource: root }) : this.configurationService.getValue<ISearchConfiguration>()) || Object.create(null),
|
||||
event => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) || event.affectsConfiguration(SEARCH_EXCLUDE_CONFIG)
|
||||
));
|
||||
|
||||
this._register(matcher.onExpressionChange(() => this.removeExcludedFromHistory()));
|
||||
|
||||
return matcher;
|
||||
}));
|
||||
|
||||
private handleEditorEventInHistory(editor?: IEditorPane): void {
|
||||
|
||||
@@ -804,7 +816,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
|
||||
const resourceEditorInput = input as IResourceEditorInput;
|
||||
|
||||
return !this.resourceExcludeMatcher.matches(resourceEditorInput.resource);
|
||||
return !this.resourceExcludeMatcher.value.matches(resourceEditorInput.resource);
|
||||
}
|
||||
|
||||
private removeExcludedFromHistory(): void {
|
||||
|
||||
@@ -174,8 +174,8 @@ export class BrowserHostService extends Disposable implements IHostService {
|
||||
// New Window: open into empty window
|
||||
else {
|
||||
const environment = new Map<string, string>();
|
||||
environment.set('diffFileDetail', editors[0].resource.toString());
|
||||
environment.set('diffFileMaster', editors[1].resource.toString());
|
||||
environment.set('diffFileSecondary', editors[0].resource.toString());
|
||||
environment.set('diffFilePrimary', editors[1].resource.toString());
|
||||
|
||||
this.workspaceProvider.open(undefined, { payload: Array.from(environment.entries()) });
|
||||
}
|
||||
@@ -283,7 +283,7 @@ export class BrowserHostService extends Disposable implements IHostService {
|
||||
}
|
||||
|
||||
private async doOpenEmptyWindow(options?: IOpenEmptyWindowOptions): Promise<void> {
|
||||
this.workspaceProvider.open(undefined, { reuse: options?.forceReuseWindow });
|
||||
return this.workspaceProvider.open(undefined, { reuse: options?.forceReuseWindow });
|
||||
}
|
||||
|
||||
async toggleFullScreen(): Promise<void> {
|
||||
|
||||
112
src/vs/workbench/services/hover/browser/hover.ts
Normal file
112
src/vs/workbench/services/hover/browser/hover.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
|
||||
export const IHoverService = createDecorator<IHoverService>('hoverService');
|
||||
|
||||
/**
|
||||
* Enables the convenient display of rich markdown-based hovers in the workbench.
|
||||
*/
|
||||
export interface IHoverService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* Shows a hover.
|
||||
* @param options A set of options defining the characteristics of the hover.
|
||||
* @param focus Whether to focus the hover (useful for keyboard accessibility).
|
||||
*
|
||||
* **Example:** A simple usage with a single element target.
|
||||
*
|
||||
* ```typescript
|
||||
* showHover({
|
||||
* text: new MarkdownString('Hello world'),
|
||||
* target: someElement
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
showHover(options: IHoverOptions, focus?: boolean): void;
|
||||
|
||||
/**
|
||||
* Hides the hover if it was visible.
|
||||
*/
|
||||
hideHover(): void;
|
||||
}
|
||||
|
||||
export interface IHoverOptions {
|
||||
/**
|
||||
* The text to display in the primary section of the hover.
|
||||
*/
|
||||
text: IMarkdownString;
|
||||
|
||||
/**
|
||||
* The target for the hover. This determines the position of the hover and it will only be
|
||||
* hidden when the mouse leaves both the hover and the target. A HTMLElement can be used for
|
||||
* simple cases and a IHoverTarget for more complex cases where multiple elements and/or a
|
||||
* dispose method is required.
|
||||
*/
|
||||
target: IHoverTarget | HTMLElement;
|
||||
|
||||
/**
|
||||
* A set of actions for the hover's "status bar".
|
||||
*/
|
||||
actions?: IHoverAction[];
|
||||
|
||||
/**
|
||||
* An optional array of classes to add to the hover element.
|
||||
*/
|
||||
additionalClasses?: string[];
|
||||
|
||||
/**
|
||||
* An optional link handler for markdown links, if this is not provided the IOpenerService will
|
||||
* be used to open the links using its default options.
|
||||
*/
|
||||
linkHandler?(url: string): void;
|
||||
|
||||
/**
|
||||
* Whether to hide the hover when the mouse leaves the `target` and enters the actual hover.
|
||||
* This is false by default and note that it will be ignored if any `actions` are provided such
|
||||
* that they are accessible.
|
||||
*/
|
||||
hideOnHover?: boolean;
|
||||
}
|
||||
|
||||
export interface IHoverAction {
|
||||
/**
|
||||
* The label to use in the hover's status bar.
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* The command ID of the action, this is used to resolve the keybinding to display after the
|
||||
* action label.
|
||||
*/
|
||||
commandId: string;
|
||||
|
||||
/**
|
||||
* An optional class of an icon that will be displayed before the label.
|
||||
*/
|
||||
iconClass?: string;
|
||||
|
||||
/**
|
||||
* The callback to run the action.
|
||||
* @param target The action element that was activated.
|
||||
*/
|
||||
run(target: HTMLElement): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A target for a hover.
|
||||
*/
|
||||
export interface IHoverTarget extends IDisposable {
|
||||
/**
|
||||
* A set of target elements used to position the hover. If multiple elements are used the hover
|
||||
* will try to not overlap any target element. An example use case for this is show a hover for
|
||||
* wrapped text.
|
||||
*/
|
||||
readonly targetElements: readonly HTMLElement[];
|
||||
}
|
||||
111
src/vs/workbench/services/hover/browser/hoverService.ts
Normal file
111
src/vs/workbench/services/hover/browser/hoverService.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/hover';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorHoverBackground, editorHoverBorder, textLinkForeground, editorHoverForeground, editorHoverStatusBarBackground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IHoverService, IHoverOptions } from 'vs/workbench/services/hover/browser/hover';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { HoverWidget } from 'vs/workbench/services/hover/browser/hoverWidget';
|
||||
import { IContextViewProvider, IDelegate } from 'vs/base/browser/ui/contextview/contextview';
|
||||
|
||||
export class HoverService implements IHoverService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private _currentHoverOptions: IHoverOptions | undefined;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IContextViewService private readonly _contextViewService: IContextViewService
|
||||
) {
|
||||
}
|
||||
|
||||
showHover(options: IHoverOptions, focus?: boolean): void {
|
||||
if (this._currentHoverOptions === options) {
|
||||
return;
|
||||
}
|
||||
this._currentHoverOptions = options;
|
||||
|
||||
const hover = this._instantiationService.createInstance(HoverWidget, options);
|
||||
hover.onDispose(() => this._currentHoverOptions = undefined);
|
||||
const provider = this._contextViewService as IContextViewProvider;
|
||||
provider.showContextView(new HoverContextViewDelegate(hover, focus));
|
||||
hover.onRequestLayout(() => provider.layout());
|
||||
}
|
||||
|
||||
hideHover(): void {
|
||||
if (!this._currentHoverOptions) {
|
||||
return;
|
||||
}
|
||||
this._currentHoverOptions = undefined;
|
||||
this._contextViewService.hideContextView();
|
||||
}
|
||||
}
|
||||
|
||||
class HoverContextViewDelegate implements IDelegate {
|
||||
|
||||
get anchorPosition() {
|
||||
return this._hover.anchor;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _hover: HoverWidget,
|
||||
private readonly _focus: boolean = false
|
||||
) {
|
||||
}
|
||||
|
||||
render(container: HTMLElement) {
|
||||
this._hover.render(container);
|
||||
if (this._focus) {
|
||||
this._hover.focus();
|
||||
}
|
||||
return this._hover;
|
||||
}
|
||||
|
||||
getAnchor() {
|
||||
return {
|
||||
x: this._hover.x,
|
||||
y: this._hover.y
|
||||
};
|
||||
}
|
||||
|
||||
layout() {
|
||||
this._hover.layout();
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IHoverService, HoverService, true);
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const hoverBackground = theme.getColor(editorHoverBackground);
|
||||
if (hoverBackground) {
|
||||
collector.addRule(`.monaco-workbench .workbench-hover { background-color: ${hoverBackground}; }`);
|
||||
}
|
||||
const hoverBorder = theme.getColor(editorHoverBorder);
|
||||
if (hoverBorder) {
|
||||
collector.addRule(`.monaco-workbench .workbench-hover { border: 1px solid ${hoverBorder}; }`);
|
||||
collector.addRule(`.monaco-workbench .workbench-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);
|
||||
collector.addRule(`.monaco-workbench .workbench-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);
|
||||
collector.addRule(`.monaco-workbench .workbench-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`);
|
||||
}
|
||||
const link = theme.getColor(textLinkForeground);
|
||||
if (link) {
|
||||
collector.addRule(`.monaco-workbench .workbench-hover a { color: ${link}; }`);
|
||||
}
|
||||
const hoverForeground = theme.getColor(editorHoverForeground);
|
||||
if (hoverForeground) {
|
||||
collector.addRule(`.monaco-workbench .workbench-hover { color: ${hoverForeground}; }`);
|
||||
}
|
||||
const actionsBackground = theme.getColor(editorHoverStatusBarBackground);
|
||||
if (actionsBackground) {
|
||||
collector.addRule(`.monaco-workbench .workbench-hover .hover-row .actions { background-color: ${actionsBackground}; }`);
|
||||
}
|
||||
const codeBackground = theme.getColor(textCodeBlockBackground);
|
||||
if (codeBackground) {
|
||||
collector.addRule(`.monaco-workbench .workbench-hover code { background-color: ${codeBackground}; }`);
|
||||
}
|
||||
});
|
||||
238
src/vs/workbench/services/hover/browser/hoverWidget.ts
Normal file
238
src/vs/workbench/services/hover/browser/hoverWidget.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IHoverTarget, IHoverOptions } from 'vs/workbench/services/hover/browser/hover';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { HoverWidget as BaseHoverWidget, renderHoverAction } from 'vs/base/browser/ui/hover/hoverWidget';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export class HoverWidget extends Widget {
|
||||
private readonly _messageListeners = new DisposableStore();
|
||||
private readonly _mouseTracker: CompositeMouseTracker;
|
||||
|
||||
private readonly _hover: BaseHoverWidget;
|
||||
private readonly _target: IHoverTarget;
|
||||
private readonly _linkHandler: (url: string) => any;
|
||||
|
||||
private _isDisposed: boolean = false;
|
||||
private _anchor: AnchorPosition = AnchorPosition.ABOVE;
|
||||
private _x: number = 0;
|
||||
private _y: number = 0;
|
||||
|
||||
get isDisposed(): boolean { return this._isDisposed; }
|
||||
get domNode(): HTMLElement { return this._hover.containerDomNode; }
|
||||
|
||||
private readonly _onDispose = this._register(new Emitter<void>());
|
||||
get onDispose(): Event<void> { return this._onDispose.event; }
|
||||
private readonly _onRequestLayout = this._register(new Emitter<void>());
|
||||
get onRequestLayout(): Event<void> { return this._onRequestLayout.event; }
|
||||
|
||||
get anchor(): AnchorPosition { return this._anchor; }
|
||||
get x(): number { return this._x; }
|
||||
get y(): number { return this._y; }
|
||||
|
||||
constructor(
|
||||
options: IHoverOptions,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IOpenerService private readonly _openerService: IOpenerService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._linkHandler = options.linkHandler || this._openerService.open;
|
||||
|
||||
this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target);
|
||||
|
||||
this._hover = this._register(new BaseHoverWidget());
|
||||
|
||||
this._hover.containerDomNode.classList.add('workbench-hover', 'fadeIn');
|
||||
if (options.additionalClasses) {
|
||||
this._hover.containerDomNode.classList.add(...options.additionalClasses);
|
||||
}
|
||||
|
||||
// Don't allow mousedown out of the widget, otherwise preventDefault will call and text will
|
||||
// not be selected.
|
||||
this.onmousedown(this._hover.containerDomNode, e => e.stopPropagation());
|
||||
|
||||
// Hide hover on escape
|
||||
this.onkeydown(this._hover.containerDomNode, e => {
|
||||
if (e.equals(KeyCode.Escape)) {
|
||||
this.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
const rowElement = $('div.hover-row.markdown-hover');
|
||||
const contentsElement = $('div.hover-contents');
|
||||
const markdownElement = renderMarkdown(options.text, {
|
||||
actionHandler: {
|
||||
callback: (content) => this._linkHandler(content),
|
||||
disposeables: this._messageListeners
|
||||
},
|
||||
codeBlockRenderer: async (_, value) => {
|
||||
const fontFamily = this._configurationService.getValue<IEditorOptions>('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily;
|
||||
return `<span style="font-family: ${fontFamily}; white-space: nowrap">${value.replace(/\n/g, '<br>')}</span>`;
|
||||
},
|
||||
codeBlockRenderCallback: () => {
|
||||
contentsElement.classList.add('code-hover-contents');
|
||||
// This changes the dimensions of the hover so trigger a layout
|
||||
this._onRequestLayout.fire();
|
||||
}
|
||||
});
|
||||
contentsElement.appendChild(markdownElement);
|
||||
rowElement.appendChild(contentsElement);
|
||||
this._hover.contentsDomNode.appendChild(rowElement);
|
||||
|
||||
if (options.actions && options.actions.length > 0) {
|
||||
const statusBarElement = $('div.hover-row.status-bar');
|
||||
const actionsElement = $('div.actions');
|
||||
options.actions.forEach(action => {
|
||||
const keybinding = this._keybindingService.lookupKeybinding(action.commandId);
|
||||
const keybindingLabel = keybinding ? keybinding.getLabel() : null;
|
||||
renderHoverAction(actionsElement, {
|
||||
label: action.label,
|
||||
commandId: action.commandId,
|
||||
run: e => {
|
||||
action.run(e);
|
||||
this.dispose();
|
||||
},
|
||||
iconClass: action.iconClass
|
||||
}, keybindingLabel);
|
||||
});
|
||||
statusBarElement.appendChild(actionsElement);
|
||||
this._hover.containerDomNode.appendChild(statusBarElement);
|
||||
}
|
||||
|
||||
const mouseTrackerTargets = [...this._target.targetElements];
|
||||
if (!options.hideOnHover || (options.actions && options.actions.length > 0)) {
|
||||
mouseTrackerTargets.push(this._hover.containerDomNode);
|
||||
}
|
||||
this._mouseTracker = new CompositeMouseTracker(mouseTrackerTargets);
|
||||
this._register(this._mouseTracker.onMouseOut(() => this.dispose()));
|
||||
this._register(this._mouseTracker);
|
||||
}
|
||||
|
||||
public render(container?: HTMLElement): void {
|
||||
if (this._hover.containerDomNode.parentElement !== container) {
|
||||
container?.appendChild(this._hover.containerDomNode);
|
||||
}
|
||||
|
||||
this.layout();
|
||||
}
|
||||
|
||||
public layout() {
|
||||
this._hover.containerDomNode.classList.remove('right-aligned');
|
||||
this._hover.contentsDomNode.style.maxHeight = '';
|
||||
|
||||
// Get horizontal alignment and position
|
||||
const targetBounds = this._target.targetElements.map(e => e.getBoundingClientRect());
|
||||
const targetLeft = Math.min(...targetBounds.map(e => e.left));
|
||||
if (targetLeft + this._hover.containerDomNode.clientWidth >= document.documentElement.clientWidth) {
|
||||
this._x = document.documentElement.clientWidth;
|
||||
this._hover.containerDomNode.classList.add('right-aligned');
|
||||
} else {
|
||||
this._x = targetLeft;
|
||||
}
|
||||
|
||||
// Get vertical alignment and position
|
||||
const targetTop = Math.min(...targetBounds.map(e => e.top));
|
||||
if (targetTop - this._hover.containerDomNode.clientHeight < 0) {
|
||||
this._anchor = AnchorPosition.BELOW;
|
||||
this._y = Math.max(...targetBounds.map(e => e.bottom)) - 2;
|
||||
} else {
|
||||
this._y = targetTop;
|
||||
}
|
||||
|
||||
this._hover.onContentsChanged();
|
||||
}
|
||||
|
||||
public focus() {
|
||||
this._hover.containerDomNode.focus();
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (!this._isDisposed) {
|
||||
this._onDispose.fire();
|
||||
this._hover.containerDomNode.parentElement?.removeChild(this.domNode);
|
||||
this._messageListeners.dispose();
|
||||
this._target.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
this._isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
class CompositeMouseTracker extends Widget {
|
||||
private _isMouseIn: boolean = false;
|
||||
private _mouseTimeout: number | undefined;
|
||||
|
||||
private readonly _onMouseOut = new Emitter<void>();
|
||||
get onMouseOut(): Event<void> { return this._onMouseOut.event; }
|
||||
|
||||
constructor(
|
||||
private _elements: HTMLElement[]
|
||||
) {
|
||||
super();
|
||||
this._elements.forEach(n => this.onmouseover(n, () => this._onTargetMouseOver()));
|
||||
this._elements.forEach(n => this.onnonbubblingmouseout(n, () => this._onTargetMouseOut()));
|
||||
}
|
||||
|
||||
private _onTargetMouseOver(): void {
|
||||
this._isMouseIn = true;
|
||||
this._clearEvaluateMouseStateTimeout();
|
||||
}
|
||||
|
||||
private _onTargetMouseOut(): void {
|
||||
this._isMouseIn = false;
|
||||
this._evaluateMouseState();
|
||||
}
|
||||
|
||||
private _evaluateMouseState(): void {
|
||||
this._clearEvaluateMouseStateTimeout();
|
||||
// Evaluate whether the mouse is still outside asynchronously such that other mouse targets
|
||||
// have the opportunity to first their mouse in event.
|
||||
this._mouseTimeout = window.setTimeout(() => this._fireIfMouseOutside(), 0);
|
||||
}
|
||||
|
||||
private _clearEvaluateMouseStateTimeout(): void {
|
||||
if (this._mouseTimeout) {
|
||||
clearTimeout(this._mouseTimeout);
|
||||
this._mouseTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _fireIfMouseOutside(): void {
|
||||
if (!this._isMouseIn) {
|
||||
this._onMouseOut.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ElementHoverTarget implements IHoverTarget {
|
||||
readonly targetElements: readonly HTMLElement[];
|
||||
|
||||
constructor(
|
||||
private _element: HTMLElement
|
||||
) {
|
||||
this.targetElements = [this._element];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
}
|
||||
}
|
||||
28
src/vs/workbench/services/hover/browser/media/hover.css
Normal file
28
src/vs/workbench/services/hover/browser/media/hover.css
Normal file
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .workbench-hover {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
animation: fadein 100ms linear;
|
||||
/* Must be higher than sash's z-index and terminal canvases */
|
||||
z-index: 40;
|
||||
overflow: hidden;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.monaco-workbench .workbench-hover a {
|
||||
color: #3794ff;
|
||||
}
|
||||
|
||||
.monaco-workbench .workbench-hover.right-aligned .hover-row.status-bar .actions {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.monaco-workbench .workbench-hover.right-aligned .hover-row.status-bar .actions .action-container {
|
||||
margin-right: 0;
|
||||
margin-left: 16px;
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyValueLogProvider } from 'vs/workbench/services/log/common/keyValueLogProvider';
|
||||
|
||||
export const INDEXEDDB_VSCODE_DB = 'vscode-web-db';
|
||||
export const INDEXEDDB_LOGS_OBJECT_STORE = 'vscode-logs-store';
|
||||
|
||||
export class IndexedDBLogProvider extends KeyValueLogProvider {
|
||||
|
||||
readonly database: Promise<IDBDatabase>;
|
||||
|
||||
constructor(scheme: string) {
|
||||
super(scheme);
|
||||
this.database = this.openDatabase(1);
|
||||
}
|
||||
|
||||
private openDatabase(version: number): Promise<IDBDatabase> {
|
||||
return new Promise((c, e) => {
|
||||
const request = window.indexedDB.open(INDEXEDDB_VSCODE_DB, version);
|
||||
request.onerror = (err) => e(request.error);
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
if (db.objectStoreNames.contains(INDEXEDDB_LOGS_OBJECT_STORE)) {
|
||||
c(db);
|
||||
}
|
||||
};
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(INDEXEDDB_LOGS_OBJECT_STORE)) {
|
||||
db.createObjectStore(INDEXEDDB_LOGS_OBJECT_STORE);
|
||||
}
|
||||
c(db);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected async getAllKeys(): Promise<string[]> {
|
||||
return new Promise(async (c, e) => {
|
||||
const db = await this.database;
|
||||
const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE]);
|
||||
const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE);
|
||||
const request = objectStore.getAllKeys();
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => c(<string[]>request.result);
|
||||
});
|
||||
}
|
||||
|
||||
protected hasKey(key: string): Promise<boolean> {
|
||||
return new Promise<boolean>(async (c, e) => {
|
||||
const db = await this.database;
|
||||
const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE]);
|
||||
const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE);
|
||||
const request = objectStore.getKey(key);
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => {
|
||||
c(!!request.result);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected getValue(key: string): Promise<string> {
|
||||
return new Promise(async (c, e) => {
|
||||
const db = await this.database;
|
||||
const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE]);
|
||||
const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE);
|
||||
const request = objectStore.get(key);
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => c(request.result || '');
|
||||
});
|
||||
}
|
||||
|
||||
protected setValue(key: string, value: string): Promise<void> {
|
||||
return new Promise(async (c, e) => {
|
||||
const db = await this.database;
|
||||
const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE], 'readwrite');
|
||||
const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE);
|
||||
const request = objectStore.put(value, key);
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => c();
|
||||
});
|
||||
}
|
||||
|
||||
protected deleteKey(key: string): Promise<void> {
|
||||
return new Promise(async (c, e) => {
|
||||
const db = await this.database;
|
||||
const transaction = db.transaction([INDEXEDDB_LOGS_OBJECT_STORE], 'readwrite');
|
||||
const objectStore = transaction.objectStore(INDEXEDDB_LOGS_OBJECT_STORE);
|
||||
const request = objectStore.delete(key);
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => c();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyValueLogProvider } from 'vs/workbench/services/log/common/keyValueLogProvider';
|
||||
import { keys } from 'vs/base/common/map';
|
||||
|
||||
export class InMemoryLogProvider extends KeyValueLogProvider {
|
||||
|
||||
private readonly logs: Map<string, string> = new Map<string, string>();
|
||||
|
||||
protected async getAllKeys(): Promise<string[]> {
|
||||
return keys(this.logs);
|
||||
}
|
||||
|
||||
protected async hasKey(key: string): Promise<boolean> {
|
||||
return this.logs.has(key);
|
||||
}
|
||||
|
||||
protected async getValue(key: string): Promise<string> {
|
||||
return this.logs.get(key) || '';
|
||||
}
|
||||
|
||||
protected async setValue(key: string, value: string): Promise<void> {
|
||||
this.logs.set(key, value);
|
||||
}
|
||||
|
||||
protected async deleteKey(key: string): Promise<void> {
|
||||
this.logs.delete(key);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions, FileChangeType, createFileSystemProviderError, FileSystemProviderErrorCode } from 'vs/platform/files/common/files';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { joinPath, extUri } from 'vs/base/common/resources';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export abstract class KeyValueLogProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability {
|
||||
|
||||
readonly capabilities: FileSystemProviderCapabilities =
|
||||
FileSystemProviderCapabilities.FileReadWrite
|
||||
| FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
readonly onDidChangeCapabilities: Event<void> = Event.None;
|
||||
|
||||
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
|
||||
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
|
||||
|
||||
private readonly versions: Map<string, number> = new Map<string, number>();
|
||||
|
||||
constructor(private readonly scheme: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
watch(resource: URI, opts: IWatchOptions): IDisposable {
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
async mkdir(resource: URI): Promise<void> {
|
||||
}
|
||||
|
||||
async stat(resource: URI): Promise<IStat> {
|
||||
try {
|
||||
const content = await this.readFile(resource);
|
||||
return {
|
||||
type: FileType.File,
|
||||
ctime: 0,
|
||||
mtime: this.versions.get(resource.toString()) || 0,
|
||||
size: content.byteLength
|
||||
};
|
||||
} catch (e) {
|
||||
}
|
||||
const files = await this.readdir(resource);
|
||||
if (files.length) {
|
||||
return {
|
||||
type: FileType.Directory,
|
||||
ctime: 0,
|
||||
mtime: 0,
|
||||
size: 0
|
||||
};
|
||||
}
|
||||
throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
|
||||
}
|
||||
|
||||
async readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (hasKey) {
|
||||
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
|
||||
}
|
||||
const keys = await this.getAllKeys();
|
||||
const files: Map<string, [string, FileType]> = new Map<string, [string, FileType]>();
|
||||
for (const key of keys) {
|
||||
const keyResource = this.toResource(key);
|
||||
if (extUri.isEqualOrParent(keyResource, resource)) {
|
||||
const path = extUri.relativePath(resource, keyResource);
|
||||
if (path) {
|
||||
const keySegments = path.split('/');
|
||||
files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return values(files);
|
||||
}
|
||||
|
||||
async readFile(resource: URI): Promise<Uint8Array> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (!hasKey) {
|
||||
throw createFileSystemProviderError(localize('fileNotFound', "File not found"), FileSystemProviderErrorCode.FileNotFound);
|
||||
}
|
||||
const value = await this.getValue(resource.path);
|
||||
return VSBuffer.fromString(value).buffer;
|
||||
}
|
||||
|
||||
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (!hasKey) {
|
||||
const files = await this.readdir(resource);
|
||||
if (files.length) {
|
||||
throw createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory);
|
||||
}
|
||||
}
|
||||
await this.setValue(resource.path, VSBuffer.wrap(content).toString());
|
||||
this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1);
|
||||
this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]);
|
||||
}
|
||||
|
||||
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (hasKey) {
|
||||
await this.deleteKey(resource.path);
|
||||
this.versions.delete(resource.path);
|
||||
this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.recursive) {
|
||||
const files = await this.readdir(resource);
|
||||
await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts)));
|
||||
}
|
||||
}
|
||||
|
||||
rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return Promise.reject(new Error('Not Supported'));
|
||||
}
|
||||
|
||||
private toResource(key: string): URI {
|
||||
return URI.file(key).with({ scheme: this.scheme });
|
||||
}
|
||||
|
||||
protected abstract getAllKeys(): Promise<string[]>;
|
||||
protected abstract hasKey(key: string): Promise<boolean>;
|
||||
protected abstract getValue(key: string): Promise<string>;
|
||||
protected abstract setValue(key: string, value: string): Promise<void>;
|
||||
protected abstract deleteKey(key: string): Promise<void>;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export class BrowserPathService extends AbstractPathService {
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService
|
||||
) {
|
||||
super(() => URI.from({ scheme: Schemas.vscodeRemote, authority: environmentService.configuration.remoteAuthority, path: '/' }), remoteAgentService);
|
||||
super(URI.from({ scheme: Schemas.vscodeRemote, authority: environmentService.configuration.remoteAuthority, path: '/' }), remoteAgentService);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,13 +40,13 @@ export interface IPathService {
|
||||
/**
|
||||
* Resolves the user-home directory for the target environment.
|
||||
* If the envrionment is connected to a remote, this will be the
|
||||
* remote's user home directory, otherwise the local one.
|
||||
* remote's user home directory, otherwise the local one unless
|
||||
* `preferLocal` is set to `true`.
|
||||
*/
|
||||
readonly userHome: Promise<URI>;
|
||||
userHome(options?: { preferLocal: boolean }): Promise<URI>;
|
||||
|
||||
/**
|
||||
* Access to `userHome` in a sync fashion. This may be `undefined`
|
||||
* as long as the remote environment was not resolved.
|
||||
* @deprecated use `userHome` instead.
|
||||
*/
|
||||
readonly resolvedUserHome: URI | undefined;
|
||||
}
|
||||
@@ -55,26 +55,35 @@ export abstract class AbstractPathService implements IPathService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private remoteOS: Promise<OperatingSystem>;
|
||||
private resolveOS: Promise<OperatingSystem>;
|
||||
|
||||
private resolveUserHome: Promise<URI>;
|
||||
private maybeUnresolvedUserHome: URI | undefined;
|
||||
|
||||
constructor(
|
||||
fallbackUserHome: () => URI,
|
||||
private localUserHome: URI,
|
||||
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService
|
||||
) {
|
||||
this.remoteOS = this.remoteAgentService.getEnvironment().then(env => env?.os || OS);
|
||||
|
||||
this.resolveUserHome = this.remoteAgentService.getEnvironment().then(env => {
|
||||
const userHome = this.maybeUnresolvedUserHome = env?.userHome || fallbackUserHome();
|
||||
// OS
|
||||
this.resolveOS = (async () => {
|
||||
const env = await this.remoteAgentService.getEnvironment();
|
||||
|
||||
return env?.os || OS;
|
||||
})();
|
||||
|
||||
// User Home
|
||||
this.resolveUserHome = (async () => {
|
||||
const env = await this.remoteAgentService.getEnvironment();
|
||||
const userHome = this.maybeUnresolvedUserHome = env?.userHome || localUserHome;
|
||||
|
||||
|
||||
return userHome;
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
get userHome(): Promise<URI> {
|
||||
return this.resolveUserHome;
|
||||
async userHome(options?: { preferLocal: boolean }): Promise<URI> {
|
||||
return options?.preferLocal ? this.localUserHome : this.resolveUserHome;
|
||||
}
|
||||
|
||||
get resolvedUserHome(): URI | undefined {
|
||||
@@ -82,7 +91,7 @@ export abstract class AbstractPathService implements IPathService {
|
||||
}
|
||||
|
||||
get path(): Promise<IPath> {
|
||||
return this.remoteOS.then(os => {
|
||||
return this.resolveOS.then(os => {
|
||||
return os === OperatingSystem.Windows ?
|
||||
win32 :
|
||||
posix;
|
||||
@@ -95,7 +104,8 @@ export abstract class AbstractPathService implements IPathService {
|
||||
// normalize to fwd-slashes on windows,
|
||||
// on other systems bwd-slashes are valid
|
||||
// filename character, eg /f\oo/ba\r.txt
|
||||
if ((await this.remoteOS) === OperatingSystem.Windows) {
|
||||
const os = await this.resolveOS;
|
||||
if (os === OperatingSystem.Windows) {
|
||||
_path = _path.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export class NativePathService extends AbstractPathService {
|
||||
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
|
||||
@IWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService
|
||||
) {
|
||||
super(() => environmentService.userHome, remoteAgentService);
|
||||
super(environmentService.userHome, remoteAgentService);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
}
|
||||
|
||||
const editorInput = this.getActiveSettingsEditorInput() || this.lastOpenedSettingsInput;
|
||||
const resource = editorInput ? editorInput.master.resource! : this.userSettingsResource;
|
||||
const resource = editorInput ? editorInput.primary.resource! : this.userSettingsResource;
|
||||
const target = this.getConfigurationTargetFromSettingsResource(resource);
|
||||
return this.openOrSwitchSettings(target, resource, { query: query });
|
||||
}
|
||||
@@ -317,7 +317,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
private async openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): Promise<IEditorPane | undefined> {
|
||||
const editorInput = this.getActiveSettingsEditorInput(group);
|
||||
if (editorInput) {
|
||||
const editorInputResource = editorInput.master.resource;
|
||||
const editorInputResource = editorInput.primary.resource;
|
||||
if (editorInputResource && editorInputResource.fsPath !== resource.fsPath) {
|
||||
return this.doSwitchSettings(configurationTarget, resource, editorInput, group, options);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export class PreferencesEditorInput extends SideBySideEditorInput {
|
||||
}
|
||||
|
||||
getTitle(verbosity: Verbosity): string {
|
||||
return this.master.getTitle(verbosity);
|
||||
return this.primary.getTitle(verbosity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { IEditableData } from 'vs/workbench/common/views';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TunnelInformation, TunnelDescription, IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
|
||||
export const IRemoteExplorerService = createDecorator<IRemoteExplorerService>('remoteExplorerService');
|
||||
export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType';
|
||||
@@ -141,12 +142,11 @@ export class TunnelModel extends Disposable {
|
||||
const key = MakeAddress(remote.host, remote.port);
|
||||
if (!this.forwarded.has(key)) {
|
||||
const authority = this.environmentService.configuration.remoteAuthority;
|
||||
const resolvedRemote = authority ? await this.remoteAuthorityResolverService.resolveAuthority(authority) : undefined;
|
||||
if (!resolvedRemote) {
|
||||
return;
|
||||
}
|
||||
const addressProvider: IAddressProvider | undefined = authority ? {
|
||||
getAddress: async () => { return (await this.remoteAuthorityResolverService.resolveAuthority(authority)).authority; }
|
||||
} : undefined;
|
||||
|
||||
const tunnel = await this.tunnelService.openTunnel(resolvedRemote.authority, remote.host, remote.port, local);
|
||||
const tunnel = await this.tunnelService.openTunnel(addressProvider, remote.host, remote.port, local);
|
||||
if (tunnel && tunnel.localAddress) {
|
||||
const newForward: Tunnel = {
|
||||
remoteHost: tunnel.tunnelRemoteHost,
|
||||
|
||||
@@ -11,13 +11,11 @@ import * as objects from 'vs/base/common/objects';
|
||||
import * as extpath from 'vs/base/common/extpath';
|
||||
import { fuzzyContains, getNLines } from 'vs/base/common/strings';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { IFilesConfiguration, FILES_EXCLUDE_CONFIG } from 'vs/platform/files/common/files';
|
||||
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IFilesConfiguration } from 'vs/platform/files/common/files';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { relative } from 'vs/base/common/path';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ResourceGlobMatcher } from 'vs/workbench/common/resources';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
|
||||
export const VIEWLET_ID = 'workbench.view.search';
|
||||
@@ -384,14 +382,6 @@ export function getExcludes(configuration: ISearchConfiguration, includeSearchEx
|
||||
return allExcludes;
|
||||
}
|
||||
|
||||
export function createResourceExcludeMatcher(instantiationService: IInstantiationService, configurationService: IConfigurationService): ResourceGlobMatcher {
|
||||
return instantiationService.createInstance(
|
||||
ResourceGlobMatcher,
|
||||
root => getExcludes(root ? configurationService.getValue<ISearchConfiguration>({ resource: root }) : configurationService.getValue<ISearchConfiguration>()) || Object.create(null),
|
||||
event => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) || event.affectsConfiguration(SEARCH_EXCLUDE_CONFIG)
|
||||
);
|
||||
}
|
||||
|
||||
export function pathIncludedInQuery(queryProps: ICommonQueryProps<URI>, fsPath: string): boolean {
|
||||
if (queryProps.excludePattern && glob.match(queryProps.excludePattern, fsPath)) {
|
||||
return false;
|
||||
|
||||
@@ -116,7 +116,8 @@ export class RipgrepTextSearchEngine {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the first line of stderr and return an error for display or undefined, based on a whitelist.
|
||||
* Read the first line of stderr and return an error for display or undefined, based on a list of
|
||||
* allowed properties.
|
||||
* Ripgrep produces stderr output which is not from a fatal error, and we only want the search to be
|
||||
* "failed" when a fatal error was produced.
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { toCanonicalName } from 'vs/base/node/encoding';
|
||||
import { toCanonicalName } from 'vs/workbench/services/textfile/common/encoding';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { ITextQuery } from 'vs/workbench/services/search/common/search';
|
||||
import { TextSearchProvider } from 'vs/workbench/services/search/common/searchExtTypes';
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService';
|
||||
import { ITextFileService, IResourceEncodings, IResourceEncoding, TextFileEditorModelState } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { AbstractTextFileService, EncodingOracle } from 'vs/workbench/services/textfile/browser/textFileService';
|
||||
import { ITextFileService, IResourceEncoding, TextFileEditorModelState } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
|
||||
export class BrowserTextFileService extends AbstractTextFileService {
|
||||
|
||||
readonly encoding: IResourceEncodings = {
|
||||
async getPreferredWriteEncoding(): Promise<IResourceEncoding> {
|
||||
return { encoding: 'utf8', hasBOM: false };
|
||||
private _browserEncoding: EncodingOracle | undefined;
|
||||
|
||||
get encoding(): EncodingOracle {
|
||||
if (!this._browserEncoding) {
|
||||
this._browserEncoding = this._register(this.instantiationService.createInstance(BrowserEncodingOracle));
|
||||
}
|
||||
};
|
||||
|
||||
return this._browserEncoding;
|
||||
}
|
||||
|
||||
protected registerListeners(): void {
|
||||
super.registerListeners();
|
||||
@@ -34,4 +38,18 @@ export class BrowserTextFileService extends AbstractTextFileService {
|
||||
}
|
||||
}
|
||||
|
||||
class BrowserEncodingOracle extends EncodingOracle {
|
||||
async getPreferredWriteEncoding(): Promise<IResourceEncoding> {
|
||||
return { encoding: 'utf8', hasBOM: false };
|
||||
}
|
||||
|
||||
async getWriteEncoding(): Promise<{ encoding: string, addBOM: boolean }> {
|
||||
return { encoding: 'utf8', addBOM: false };
|
||||
}
|
||||
|
||||
async getReadEncoding(): Promise<string> {
|
||||
return 'utf8';
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ITextFileService, BrowserTextFileService);
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { AsyncEmitter } from 'vs/base/common/event';
|
||||
import { ITextFileService, ITextFileStreamContent, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, ITextFileSaveOptions, ITextFileEditorModelManager, TextFileCreateEvent } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ITextFileService, ITextFileStreamContent, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, ITextFileSaveOptions, ITextFileEditorModelManager, IResourceEncoding } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IRevertOptions, IEncodingSupport } from 'vs/workbench/common/editor';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files';
|
||||
import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions } from 'vs/platform/files/common/files';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IUntitledTextEditorService, IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
|
||||
@@ -19,14 +18,13 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { joinPath, dirname, basename, toLocalResource } from 'vs/base/common/resources';
|
||||
import { joinPath, dirname, basename, toLocalResource, extUri, extname, isEqualOrParent } from 'vs/base/common/resources';
|
||||
import { IDialogService, IFileDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer';
|
||||
import { ITextSnapshot, ITextModel } from 'vs/editor/common/model';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
|
||||
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
|
||||
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
@@ -35,6 +33,10 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService';
|
||||
import { isValidBasename } from 'vs/base/common/extpath';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { UTF8, UTF8_with_bom, UTF16be, UTF16le, encodingExists, UTF8_BOM, detectEncodingByBOMFromBuffer } from 'vs/workbench/services/textfile/common/encoding';
|
||||
|
||||
/**
|
||||
* The workbench file service implementation implements the raw file service spec and adds additional methods on top.
|
||||
@@ -43,19 +45,10 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
//#region events
|
||||
|
||||
private _onDidCreateTextFile = this._register(new AsyncEmitter<TextFileCreateEvent>());
|
||||
readonly onDidCreateTextFile = this._onDidCreateTextFile.event;
|
||||
|
||||
//#endregion
|
||||
|
||||
readonly files: ITextFileEditorModelManager = this._register(this.instantiationService.createInstance(TextFileEditorModelManager));
|
||||
|
||||
readonly untitled: IUntitledTextEditorModelManager = this.untitledTextEditorService;
|
||||
|
||||
abstract get encoding(): IResourceEncodings;
|
||||
|
||||
constructor(
|
||||
@IFileService protected readonly fileService: IFileService,
|
||||
@IUntitledTextEditorService private untitledTextEditorService: IUntitledTextEditorService,
|
||||
@@ -86,6 +79,16 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
|
||||
|
||||
//#region text file read / write / create
|
||||
|
||||
private _encoding: EncodingOracle | undefined;
|
||||
|
||||
get encoding(): EncodingOracle {
|
||||
if (!this._encoding) {
|
||||
this._encoding = this._register(this.instantiationService.createInstance(EncodingOracle));
|
||||
}
|
||||
|
||||
return this._encoding;
|
||||
}
|
||||
|
||||
async read(resource: URI, options?: IReadTextFileOptions): Promise<ITextFileContent> {
|
||||
const content = await this.fileService.readFile(resource, options);
|
||||
|
||||
@@ -141,30 +144,17 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
|
||||
}
|
||||
|
||||
async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
|
||||
const encodedValue = await this.doEncodeText(resource, value);
|
||||
|
||||
// file operation participation
|
||||
await this.workingCopyFileService.runFileOperationParticipants(resource, undefined, FileOperation.CREATE);
|
||||
|
||||
// create file on disk
|
||||
const stat = await this.doCreate(resource, value, options);
|
||||
|
||||
// If we had an existing model for the given resource, load
|
||||
// it again to make sure it is up to date with the contents
|
||||
// we just wrote into the underlying resource by calling
|
||||
// revert()
|
||||
const existingModel = this.files.get(resource);
|
||||
if (existingModel && !existingModel.isDisposed()) {
|
||||
await existingModel.revert();
|
||||
}
|
||||
|
||||
// after event
|
||||
await this._onDidCreateTextFile.fireAsync({ resource }, CancellationToken.None);
|
||||
|
||||
return stat;
|
||||
return this.workingCopyFileService.create(resource, encodedValue, options);
|
||||
}
|
||||
|
||||
protected doCreate(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
|
||||
return this.fileService.createFile(resource, toBufferOrReadable(value), options);
|
||||
protected async doEncodeText(resource: URI, value?: string | ITextSnapshot): Promise<VSBuffer | VSBufferReadable | undefined> {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return toBufferOrReadable(value);
|
||||
}
|
||||
|
||||
async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
|
||||
@@ -224,7 +214,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
|
||||
}
|
||||
|
||||
// Just save if target is same as models own resource
|
||||
if (source.toString() === target.toString()) {
|
||||
if (extUri.isEqual(source, target)) {
|
||||
return this.save(source, { ...options, force: true /* force to save, even if not dirty (https://github.com/microsoft/vscode/issues/99619) */ });
|
||||
}
|
||||
|
||||
@@ -234,7 +224,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
|
||||
// However, this will only work if the source exists
|
||||
// and is not orphaned, so we need to check that too.
|
||||
if (this.fileService.canHandleResource(source) && this.uriIdentityService.extUri.isEqual(source, target) && (await this.fileService.exists(source))) {
|
||||
await this.workingCopyFileService.move(source, target);
|
||||
await this.workingCopyFileService.move([{ source, target }]);
|
||||
|
||||
return this.save(target, options);
|
||||
}
|
||||
@@ -450,7 +440,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
|
||||
|
||||
// Try to place where last active file was if any
|
||||
// Otherwise fallback to user home
|
||||
return joinPath(this.fileDialogService.defaultFilePath() || (await this.pathService.userHome), suggestedFilename);
|
||||
return joinPath(this.fileDialogService.defaultFilePath() || (await this.pathService.userHome()), suggestedFilename);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
@@ -491,3 +481,150 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
export interface IEncodingOverride {
|
||||
parent?: URI;
|
||||
extension?: string;
|
||||
encoding: string;
|
||||
}
|
||||
|
||||
export class EncodingOracle extends Disposable implements IResourceEncodings {
|
||||
|
||||
private _encodingOverrides: IEncodingOverride[];
|
||||
protected get encodingOverrides(): IEncodingOverride[] { return this._encodingOverrides; }
|
||||
protected set encodingOverrides(value: IEncodingOverride[]) { this._encodingOverrides = value; }
|
||||
|
||||
constructor(
|
||||
@ITextResourceConfigurationService private textResourceConfigurationService: ITextResourceConfigurationService,
|
||||
@IEnvironmentService private environmentService: IEnvironmentService,
|
||||
@IWorkspaceContextService private contextService: IWorkspaceContextService,
|
||||
@IFileService private fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._encodingOverrides = this.getDefaultEncodingOverrides();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// Workspace Folder Change
|
||||
this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.encodingOverrides = this.getDefaultEncodingOverrides()));
|
||||
}
|
||||
|
||||
private getDefaultEncodingOverrides(): IEncodingOverride[] {
|
||||
const defaultEncodingOverrides: IEncodingOverride[] = [];
|
||||
|
||||
// Global settings
|
||||
defaultEncodingOverrides.push({ parent: this.environmentService.userRoamingDataHome, encoding: UTF8 });
|
||||
|
||||
// Workspace files (via extension and via untitled workspaces location)
|
||||
defaultEncodingOverrides.push({ extension: WORKSPACE_EXTENSION, encoding: UTF8 });
|
||||
defaultEncodingOverrides.push({ parent: this.environmentService.untitledWorkspacesHome, encoding: UTF8 });
|
||||
|
||||
// Folder Settings
|
||||
this.contextService.getWorkspace().folders.forEach(folder => {
|
||||
defaultEncodingOverrides.push({ parent: joinPath(folder.uri, '.vscode'), encoding: UTF8 });
|
||||
});
|
||||
|
||||
return defaultEncodingOverrides;
|
||||
}
|
||||
|
||||
async getWriteEncoding(resource: URI, options?: IWriteTextFileOptions): Promise<{ encoding: string, addBOM: boolean }> {
|
||||
const { encoding, hasBOM } = await this.getPreferredWriteEncoding(resource, options ? options.encoding : undefined);
|
||||
|
||||
// Some encodings come with a BOM automatically
|
||||
if (hasBOM) {
|
||||
return { encoding, addBOM: true };
|
||||
}
|
||||
|
||||
// Ensure that we preserve an existing BOM if found for UTF8
|
||||
// unless we are instructed to overwrite the encoding
|
||||
const overwriteEncoding = options?.overwriteEncoding;
|
||||
if (!overwriteEncoding && encoding === UTF8) {
|
||||
try {
|
||||
const buffer = (await this.fileService.readFile(resource, { length: UTF8_BOM.length })).value;
|
||||
if (detectEncodingByBOMFromBuffer(buffer, buffer.byteLength) === UTF8_with_bom) {
|
||||
return { encoding, addBOM: true };
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore - file might not exist
|
||||
}
|
||||
}
|
||||
|
||||
return { encoding, addBOM: false };
|
||||
}
|
||||
|
||||
async getPreferredWriteEncoding(resource: URI, preferredEncoding?: string): Promise<IResourceEncoding> {
|
||||
const resourceEncoding = await this.getEncodingForResource(resource, preferredEncoding);
|
||||
|
||||
return {
|
||||
encoding: resourceEncoding,
|
||||
hasBOM: resourceEncoding === UTF16be || resourceEncoding === UTF16le || resourceEncoding === UTF8_with_bom // enforce BOM for certain encodings
|
||||
};
|
||||
}
|
||||
|
||||
getReadEncoding(resource: URI, options: IReadTextFileOptions | undefined, detectedEncoding: string | null): Promise<string> {
|
||||
let preferredEncoding: string | undefined;
|
||||
|
||||
// Encoding passed in as option
|
||||
if (options?.encoding) {
|
||||
if (detectedEncoding === UTF8_with_bom && options.encoding === UTF8) {
|
||||
preferredEncoding = UTF8_with_bom; // indicate the file has BOM if we are to resolve with UTF 8
|
||||
} else {
|
||||
preferredEncoding = options.encoding; // give passed in encoding highest priority
|
||||
}
|
||||
}
|
||||
|
||||
// Encoding detected
|
||||
else if (detectedEncoding) {
|
||||
preferredEncoding = detectedEncoding;
|
||||
}
|
||||
|
||||
// Encoding configured
|
||||
else if (this.textResourceConfigurationService.getValue(resource, 'files.encoding') === UTF8_with_bom) {
|
||||
preferredEncoding = UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then
|
||||
}
|
||||
|
||||
return this.getEncodingForResource(resource, preferredEncoding);
|
||||
}
|
||||
|
||||
private async getEncodingForResource(resource: URI, preferredEncoding?: string): Promise<string> {
|
||||
let fileEncoding: string;
|
||||
|
||||
const override = this.getEncodingOverride(resource);
|
||||
if (override) {
|
||||
fileEncoding = override; // encoding override always wins
|
||||
} else if (preferredEncoding) {
|
||||
fileEncoding = preferredEncoding; // preferred encoding comes second
|
||||
} else {
|
||||
fileEncoding = this.textResourceConfigurationService.getValue(resource, 'files.encoding'); // and last we check for settings
|
||||
}
|
||||
|
||||
if (!fileEncoding || !(await encodingExists(fileEncoding))) {
|
||||
fileEncoding = UTF8; // the default is UTF 8
|
||||
}
|
||||
|
||||
return fileEncoding;
|
||||
}
|
||||
|
||||
private getEncodingOverride(resource: URI): string | undefined {
|
||||
if (this.encodingOverrides && this.encodingOverrides.length) {
|
||||
for (const override of this.encodingOverrides) {
|
||||
|
||||
// check if the resource is child of encoding override path
|
||||
if (override.parent && isEqualOrParent(resource, override.parent)) {
|
||||
return override.encoding;
|
||||
}
|
||||
|
||||
// check if the resource extension is equal to encoding override
|
||||
if (override.extension && extname(resource) === `.${override.extension}`) {
|
||||
return override.encoding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
383
src/vs/workbench/services/textfile/common/encoding.ts
Normal file
383
src/vs/workbench/services/textfile/common/encoding.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DecoderStream } from 'iconv-lite-umd';
|
||||
import { Readable, ReadableStream, newWriteableStream } from 'vs/base/common/stream';
|
||||
import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
|
||||
export const UTF8 = 'utf8';
|
||||
export const UTF8_with_bom = 'utf8bom';
|
||||
export const UTF16be = 'utf16be';
|
||||
export const UTF16le = 'utf16le';
|
||||
|
||||
export type UTF_ENCODING = typeof UTF8 | typeof UTF8_with_bom | typeof UTF16be | typeof UTF16le;
|
||||
|
||||
export function isUTFEncoding(encoding: string): encoding is UTF_ENCODING {
|
||||
return [UTF8, UTF8_with_bom, UTF16be, UTF16le].some(utfEncoding => utfEncoding === encoding);
|
||||
}
|
||||
|
||||
export const UTF16be_BOM = [0xFE, 0xFF];
|
||||
export const UTF16le_BOM = [0xFF, 0xFE];
|
||||
export const UTF8_BOM = [0xEF, 0xBB, 0xBF];
|
||||
|
||||
const ZERO_BYTE_DETECTION_BUFFER_MAX_LEN = 512; // number of bytes to look at to decide about a file being binary or not
|
||||
const NO_ENCODING_GUESS_MIN_BYTES = 512; // when not auto guessing the encoding, small number of bytes are enough
|
||||
const AUTO_ENCODING_GUESS_MIN_BYTES = 512 * 8; // with auto guessing we want a lot more content to be read for guessing
|
||||
const AUTO_ENCODING_GUESS_MAX_BYTES = 512 * 128; // set an upper limit for the number of bytes we pass on to jschardet
|
||||
|
||||
export interface IDecodeStreamOptions {
|
||||
guessEncoding: boolean;
|
||||
minBytesRequiredForDetection?: number;
|
||||
|
||||
overwriteEncoding(detectedEncoding: string | null): Promise<string>;
|
||||
}
|
||||
|
||||
export interface IDecodeStreamResult {
|
||||
stream: ReadableStream<string>;
|
||||
detected: IDetectedEncodingResult;
|
||||
}
|
||||
|
||||
export function toDecodeStream(source: VSBufferReadableStream, options: IDecodeStreamOptions): Promise<IDecodeStreamResult> {
|
||||
const minBytesRequiredForDetection = options.minBytesRequiredForDetection ?? options.guessEncoding ? AUTO_ENCODING_GUESS_MIN_BYTES : NO_ENCODING_GUESS_MIN_BYTES;
|
||||
|
||||
return new Promise<IDecodeStreamResult>((resolve, reject) => {
|
||||
const target = newWriteableStream<string>(strings => strings.join(''));
|
||||
|
||||
const bufferedChunks: VSBuffer[] = [];
|
||||
let bytesBuffered = 0;
|
||||
|
||||
let decoder: DecoderStream | undefined = undefined;
|
||||
|
||||
const createDecoder = async () => {
|
||||
try {
|
||||
|
||||
// detect encoding from buffer
|
||||
const detected = await detectEncodingFromBuffer({
|
||||
buffer: VSBuffer.concat(bufferedChunks),
|
||||
bytesRead: bytesBuffered
|
||||
}, options.guessEncoding);
|
||||
|
||||
// ensure to respect overwrite of encoding
|
||||
detected.encoding = await options.overwriteEncoding(detected.encoding);
|
||||
|
||||
// decode and write buffered content
|
||||
const iconv = await import('iconv-lite-umd');
|
||||
decoder = iconv.getDecoder(toNodeEncoding(detected.encoding));
|
||||
const decoded = decoder.write(VSBuffer.concat(bufferedChunks).buffer);
|
||||
target.write(decoded);
|
||||
|
||||
bufferedChunks.length = 0;
|
||||
bytesBuffered = 0;
|
||||
|
||||
// signal to the outside our detected encoding and final decoder stream
|
||||
resolve({
|
||||
stream: target,
|
||||
detected
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Stream error: forward to target
|
||||
source.on('error', error => target.error(error));
|
||||
|
||||
// Stream data
|
||||
source.on('data', async chunk => {
|
||||
|
||||
// if the decoder is ready, we just write directly
|
||||
if (decoder) {
|
||||
target.write(decoder.write(chunk.buffer));
|
||||
}
|
||||
|
||||
// otherwise we need to buffer the data until the stream is ready
|
||||
else {
|
||||
bufferedChunks.push(chunk);
|
||||
bytesBuffered += chunk.byteLength;
|
||||
|
||||
// buffered enough data for encoding detection, create stream
|
||||
if (bytesBuffered >= minBytesRequiredForDetection) {
|
||||
|
||||
// pause stream here until the decoder is ready
|
||||
source.pause();
|
||||
|
||||
await createDecoder();
|
||||
|
||||
// resume stream now that decoder is ready but
|
||||
// outside of this stack to reduce recursion
|
||||
setTimeout(() => source.resume());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Stream end
|
||||
source.on('end', async () => {
|
||||
|
||||
// we were still waiting for data to do the encoding
|
||||
// detection. thus, wrap up starting the stream even
|
||||
// without all the data to get things going
|
||||
if (!decoder) {
|
||||
await createDecoder();
|
||||
}
|
||||
|
||||
// end the target with the remainders of the decoder
|
||||
target.end(decoder?.end());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function toEncodeReadable(readable: Readable<string>, encoding: string, options?: { addBOM?: boolean }): Promise<VSBufferReadable> {
|
||||
const iconv = await import('iconv-lite-umd');
|
||||
const encoder = iconv.getEncoder(toNodeEncoding(encoding), options);
|
||||
|
||||
let bytesRead = 0;
|
||||
let done = false;
|
||||
|
||||
return {
|
||||
read() {
|
||||
if (done) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chunk = readable.read();
|
||||
if (typeof chunk !== 'string') {
|
||||
done = true;
|
||||
|
||||
// If we are instructed to add a BOM but we detect that no
|
||||
// bytes have been read, we must ensure to return the BOM
|
||||
// ourselves so that we comply with the contract.
|
||||
if (bytesRead === 0 && options?.addBOM) {
|
||||
switch (encoding) {
|
||||
case UTF8:
|
||||
case UTF8_with_bom:
|
||||
return VSBuffer.wrap(Uint8Array.from(UTF8_BOM));
|
||||
case UTF16be:
|
||||
return VSBuffer.wrap(Uint8Array.from(UTF16be_BOM));
|
||||
case UTF16le:
|
||||
return VSBuffer.wrap(Uint8Array.from(UTF16le_BOM));
|
||||
}
|
||||
}
|
||||
|
||||
const leftovers = encoder.end();
|
||||
if (leftovers && leftovers.length > 0) {
|
||||
return VSBuffer.wrap(leftovers);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
bytesRead += chunk.length;
|
||||
|
||||
return VSBuffer.wrap(encoder.write(chunk));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function encodingExists(encoding: string): Promise<boolean> {
|
||||
const iconv = await import('iconv-lite-umd');
|
||||
|
||||
return iconv.encodingExists(toNodeEncoding(encoding));
|
||||
}
|
||||
|
||||
export function toNodeEncoding(enc: string | null): string {
|
||||
if (enc === UTF8_with_bom || enc === null) {
|
||||
return UTF8; // iconv does not distinguish UTF 8 with or without BOM, so we need to help it
|
||||
}
|
||||
|
||||
return enc;
|
||||
}
|
||||
|
||||
export function detectEncodingByBOMFromBuffer(buffer: VSBuffer | null, bytesRead: number): typeof UTF8_with_bom | typeof UTF16le | typeof UTF16be | null {
|
||||
if (!buffer || bytesRead < UTF16be_BOM.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const b0 = buffer.readUInt8(0);
|
||||
const b1 = buffer.readUInt8(1);
|
||||
|
||||
// UTF-16 BE
|
||||
if (b0 === UTF16be_BOM[0] && b1 === UTF16be_BOM[1]) {
|
||||
return UTF16be;
|
||||
}
|
||||
|
||||
// UTF-16 LE
|
||||
if (b0 === UTF16le_BOM[0] && b1 === UTF16le_BOM[1]) {
|
||||
return UTF16le;
|
||||
}
|
||||
|
||||
if (bytesRead < UTF8_BOM.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const b2 = buffer.readUInt8(2);
|
||||
|
||||
// UTF-8
|
||||
if (b0 === UTF8_BOM[0] && b1 === UTF8_BOM[1] && b2 === UTF8_BOM[2]) {
|
||||
return UTF8_with_bom;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// we explicitly ignore a specific set of encodings from auto guessing
|
||||
// - ASCII: we never want this encoding (most UTF-8 files would happily detect as
|
||||
// ASCII files and then you could not type non-ASCII characters anymore)
|
||||
// - UTF-16: we have our own detection logic for UTF-16
|
||||
// - UTF-32: we do not support this encoding in VSCode
|
||||
const IGNORE_ENCODINGS = ['ascii', 'utf-16', 'utf-32'];
|
||||
|
||||
/**
|
||||
* Guesses the encoding from buffer.
|
||||
*/
|
||||
async function guessEncodingByBuffer(buffer: VSBuffer): Promise<string | null> {
|
||||
const jschardet = await import('jschardet');
|
||||
|
||||
// ensure to limit buffer for guessing due to https://github.com/aadsm/jschardet/issues/53
|
||||
const limitedBuffer = buffer.slice(0, AUTO_ENCODING_GUESS_MAX_BYTES);
|
||||
// override type since jschardet expects Buffer even though can accept Uint8Array
|
||||
// can be fixed once https://github.com/aadsm/jschardet/pull/58 is merged
|
||||
const jschardetTypingsWorkaround = limitedBuffer.buffer as any;
|
||||
|
||||
const guessed = jschardet.detect(jschardetTypingsWorkaround);
|
||||
if (!guessed || !guessed.encoding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enc = guessed.encoding.toLowerCase();
|
||||
if (0 <= IGNORE_ENCODINGS.indexOf(enc)) {
|
||||
return null; // see comment above why we ignore some encodings
|
||||
}
|
||||
|
||||
return toIconvLiteEncoding(guessed.encoding);
|
||||
}
|
||||
|
||||
const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = {
|
||||
'ibm866': 'cp866',
|
||||
'big5': 'cp950'
|
||||
};
|
||||
|
||||
function toIconvLiteEncoding(encodingName: string): string {
|
||||
const normalizedEncodingName = encodingName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
||||
const mapped = JSCHARDET_TO_ICONV_ENCODINGS[normalizedEncodingName];
|
||||
|
||||
return mapped || normalizedEncodingName;
|
||||
}
|
||||
|
||||
/**
|
||||
* The encodings that are allowed in a settings file don't match the canonical encoding labels specified by WHATWG.
|
||||
* See https://encoding.spec.whatwg.org/#names-and-labels
|
||||
* Iconv-lite strips all non-alphanumeric characters, but ripgrep doesn't. For backcompat, allow these labels.
|
||||
*/
|
||||
export function toCanonicalName(enc: string): string {
|
||||
switch (enc) {
|
||||
case 'shiftjis':
|
||||
return 'shift-jis';
|
||||
case 'utf16le':
|
||||
return 'utf-16le';
|
||||
case 'utf16be':
|
||||
return 'utf-16be';
|
||||
case 'big5hkscs':
|
||||
return 'big5-hkscs';
|
||||
case 'eucjp':
|
||||
return 'euc-jp';
|
||||
case 'euckr':
|
||||
return 'euc-kr';
|
||||
case 'koi8r':
|
||||
return 'koi8-r';
|
||||
case 'koi8u':
|
||||
return 'koi8-u';
|
||||
case 'macroman':
|
||||
return 'x-mac-roman';
|
||||
case 'utf8bom':
|
||||
return 'utf8';
|
||||
default:
|
||||
const m = enc.match(/windows(\d+)/);
|
||||
if (m) {
|
||||
return 'windows-' + m[1];
|
||||
}
|
||||
|
||||
return enc;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDetectedEncodingResult {
|
||||
encoding: string | null;
|
||||
seemsBinary: boolean;
|
||||
}
|
||||
|
||||
export interface IReadResult {
|
||||
buffer: VSBuffer | null;
|
||||
bytesRead: number;
|
||||
}
|
||||
|
||||
export function detectEncodingFromBuffer(readResult: IReadResult, autoGuessEncoding?: false): IDetectedEncodingResult;
|
||||
export function detectEncodingFromBuffer(readResult: IReadResult, autoGuessEncoding?: boolean): Promise<IDetectedEncodingResult>;
|
||||
export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, autoGuessEncoding?: boolean): Promise<IDetectedEncodingResult> | IDetectedEncodingResult {
|
||||
|
||||
// Always first check for BOM to find out about encoding
|
||||
let encoding = detectEncodingByBOMFromBuffer(buffer, bytesRead);
|
||||
|
||||
// Detect 0 bytes to see if file is binary or UTF-16 LE/BE
|
||||
// unless we already know that this file has a UTF-16 encoding
|
||||
let seemsBinary = false;
|
||||
if (encoding !== UTF16be && encoding !== UTF16le && buffer) {
|
||||
let couldBeUTF16LE = true; // e.g. 0xAA 0x00
|
||||
let couldBeUTF16BE = true; // e.g. 0x00 0xAA
|
||||
let containsZeroByte = false;
|
||||
|
||||
// This is a simplified guess to detect UTF-16 BE or LE by just checking if
|
||||
// the first 512 bytes have the 0-byte at a specific location. For UTF-16 LE
|
||||
// this would be the odd byte index and for UTF-16 BE the even one.
|
||||
// Note: this can produce false positives (a binary file that uses a 2-byte
|
||||
// encoding of the same format as UTF-16) and false negatives (a UTF-16 file
|
||||
// that is using 4 bytes to encode a character).
|
||||
for (let i = 0; i < bytesRead && i < ZERO_BYTE_DETECTION_BUFFER_MAX_LEN; i++) {
|
||||
const isEndian = (i % 2 === 1); // assume 2-byte sequences typical for UTF-16
|
||||
const isZeroByte = (buffer.readUInt8(i) === 0);
|
||||
|
||||
if (isZeroByte) {
|
||||
containsZeroByte = true;
|
||||
}
|
||||
|
||||
// UTF-16 LE: expect e.g. 0xAA 0x00
|
||||
if (couldBeUTF16LE && (isEndian && !isZeroByte || !isEndian && isZeroByte)) {
|
||||
couldBeUTF16LE = false;
|
||||
}
|
||||
|
||||
// UTF-16 BE: expect e.g. 0x00 0xAA
|
||||
if (couldBeUTF16BE && (isEndian && isZeroByte || !isEndian && !isZeroByte)) {
|
||||
couldBeUTF16BE = false;
|
||||
}
|
||||
|
||||
// Return if this is neither UTF16-LE nor UTF16-BE and thus treat as binary
|
||||
if (isZeroByte && !couldBeUTF16LE && !couldBeUTF16BE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle case of 0-byte included
|
||||
if (containsZeroByte) {
|
||||
if (couldBeUTF16LE) {
|
||||
encoding = UTF16le;
|
||||
} else if (couldBeUTF16BE) {
|
||||
encoding = UTF16be;
|
||||
} else {
|
||||
seemsBinary = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto guess encoding if configured
|
||||
if (autoGuessEncoding && !seemsBinary && !encoding && buffer) {
|
||||
return guessEncodingByBuffer(buffer.slice(0, bytesRead)).then(guessedEncoding => {
|
||||
return {
|
||||
seemsBinary: false,
|
||||
encoding: guessedEncoding
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return { seemsBinary, encoding };
|
||||
}
|
||||
@@ -135,50 +135,58 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
|
||||
private onWillRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void {
|
||||
|
||||
// Move / Copy: remember models to restore after the operation
|
||||
const source = e.source;
|
||||
if (source && (e.operation === FileOperation.COPY || e.operation === FileOperation.MOVE)) {
|
||||
|
||||
// find all models that related to either source or target (can be many if resource is a folder)
|
||||
const sourceModels: TextFileEditorModel[] = [];
|
||||
const targetModels: TextFileEditorModel[] = [];
|
||||
for (const model of this.models) {
|
||||
const resource = model.resource;
|
||||
|
||||
if (extUri.isEqualOrParent(resource, e.target)) {
|
||||
// EXPLICITLY do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384
|
||||
targetModels.push(model);
|
||||
}
|
||||
|
||||
if (this.uriIdentityService.extUri.isEqualOrParent(resource, source)) {
|
||||
sourceModels.push(model);
|
||||
}
|
||||
}
|
||||
|
||||
// remember each source model to load again after move is done
|
||||
// with optional content to restore if it was dirty
|
||||
if (e.operation === FileOperation.MOVE || e.operation === FileOperation.COPY) {
|
||||
const modelsToRestore: { source: URI, target: URI, snapshot?: ITextSnapshot; mode?: string; encoding?: string; }[] = [];
|
||||
for (const sourceModel of sourceModels) {
|
||||
const sourceModelResource = sourceModel.resource;
|
||||
|
||||
// If the source is the actual model, just use target as new resource
|
||||
let targetModelResource: URI;
|
||||
if (this.uriIdentityService.extUri.isEqual(sourceModelResource, e.source)) {
|
||||
targetModelResource = e.target;
|
||||
for (const { source, target } of e.files) {
|
||||
if (source) {
|
||||
if (this.uriIdentityService.extUri.isEqual(source, target)) {
|
||||
continue; // ignore if resources are considered equal
|
||||
}
|
||||
|
||||
// find all models that related to either source or target (can be many if resource is a folder)
|
||||
const sourceModels: TextFileEditorModel[] = [];
|
||||
const targetModels: TextFileEditorModel[] = [];
|
||||
for (const model of this.models) {
|
||||
const resource = model.resource;
|
||||
|
||||
if (extUri.isEqualOrParent(resource, target)) {
|
||||
// EXPLICITLY do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384
|
||||
targetModels.push(model);
|
||||
}
|
||||
|
||||
if (this.uriIdentityService.extUri.isEqualOrParent(resource, source)) {
|
||||
sourceModels.push(model);
|
||||
}
|
||||
}
|
||||
|
||||
// remember each source model to load again after move is done
|
||||
// with optional content to restore if it was dirty
|
||||
for (const sourceModel of sourceModels) {
|
||||
const sourceModelResource = sourceModel.resource;
|
||||
|
||||
// If the source is the actual model, just use target as new resource
|
||||
let targetModelResource: URI;
|
||||
if (this.uriIdentityService.extUri.isEqual(sourceModelResource, source)) {
|
||||
targetModelResource = target;
|
||||
}
|
||||
|
||||
// Otherwise a parent folder of the source is being moved, so we need
|
||||
// to compute the target resource based on that
|
||||
else {
|
||||
targetModelResource = joinPath(target, sourceModelResource.path.substr(source.path.length + 1));
|
||||
}
|
||||
|
||||
modelsToRestore.push({
|
||||
source: sourceModelResource,
|
||||
target: targetModelResource,
|
||||
mode: sourceModel.getMode(),
|
||||
encoding: sourceModel.getEncoding(),
|
||||
snapshot: sourceModel.isDirty() ? sourceModel.createSnapshot() : undefined
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Otherwise a parent folder of the source is being moved, so we need
|
||||
// to compute the target resource based on that
|
||||
else {
|
||||
targetModelResource = joinPath(e.target, sourceModelResource.path.substr(source.path.length + 1));
|
||||
}
|
||||
|
||||
modelsToRestore.push({
|
||||
source: sourceModelResource,
|
||||
target: targetModelResource,
|
||||
mode: sourceModel.getMode(),
|
||||
encoding: sourceModel.getEncoding(),
|
||||
snapshot: sourceModel.isDirty() ? sourceModel.createSnapshot() : undefined
|
||||
});
|
||||
}
|
||||
|
||||
this.mapCorrelationIdToModelsToRestore.set(e.correlationId, modelsToRestore);
|
||||
@@ -188,13 +196,12 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
|
||||
private onDidFailWorkingCopyFileOperation(e: WorkingCopyFileEvent): void {
|
||||
|
||||
// Move / Copy: restore dirty flag on models to restore that were dirty
|
||||
if ((e.operation === FileOperation.COPY || e.operation === FileOperation.MOVE)) {
|
||||
if ((e.operation === FileOperation.MOVE || e.operation === FileOperation.COPY)) {
|
||||
const modelsToRestore = this.mapCorrelationIdToModelsToRestore.get(e.correlationId);
|
||||
if (modelsToRestore) {
|
||||
this.mapCorrelationIdToModelsToRestore.delete(e.correlationId);
|
||||
|
||||
modelsToRestore.forEach(model => {
|
||||
|
||||
// snapshot presence means this model used to be dirty and so we restore that
|
||||
// flag. we do NOT have to restore the content because the model was only soft
|
||||
// reverted and did not loose its original dirty contents.
|
||||
@@ -207,40 +214,55 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
|
||||
}
|
||||
|
||||
private onDidRunWorkingCopyFileOperation(e: WorkingCopyFileEvent): void {
|
||||
switch (e.operation) {
|
||||
|
||||
// Move / Copy: restore models that were loaded before the operation took place
|
||||
if ((e.operation === FileOperation.COPY || e.operation === FileOperation.MOVE)) {
|
||||
e.waitUntil((async () => {
|
||||
const modelsToRestore = this.mapCorrelationIdToModelsToRestore.get(e.correlationId);
|
||||
if (modelsToRestore) {
|
||||
this.mapCorrelationIdToModelsToRestore.delete(e.correlationId);
|
||||
|
||||
await Promise.all(modelsToRestore.map(async modelToRestore => {
|
||||
|
||||
// restore the model, forcing a reload. this is important because
|
||||
// we know the file has changed on disk after the move and the
|
||||
// model might have still existed with the previous state. this
|
||||
// ensures we are not tracking a stale state.
|
||||
const restoredModel = await this.resolve(modelToRestore.target, { reload: { async: false }, encoding: modelToRestore.encoding });
|
||||
|
||||
// restore previous dirty content if any and ensure to mark the model as dirty
|
||||
let textBufferFactory: ITextBufferFactory | undefined = undefined;
|
||||
if (modelToRestore.snapshot) {
|
||||
textBufferFactory = createTextBufferFactoryFromSnapshot(modelToRestore.snapshot);
|
||||
// Create: Revert existing models
|
||||
case FileOperation.CREATE:
|
||||
e.waitUntil((async () => {
|
||||
for (const { target } of e.files) {
|
||||
const model = this.get(target);
|
||||
if (model && !model.isDisposed()) {
|
||||
await model.revert();
|
||||
}
|
||||
}
|
||||
})());
|
||||
break;
|
||||
|
||||
// restore previous mode only if the mode is now unspecified
|
||||
let preferredMode: string | undefined = undefined;
|
||||
if (restoredModel.getMode() === PLAINTEXT_MODE_ID && modelToRestore.mode !== PLAINTEXT_MODE_ID) {
|
||||
preferredMode = modelToRestore.mode;
|
||||
}
|
||||
// Move/Copy: restore models that were loaded before the operation took place
|
||||
case FileOperation.MOVE:
|
||||
case FileOperation.COPY:
|
||||
e.waitUntil((async () => {
|
||||
const modelsToRestore = this.mapCorrelationIdToModelsToRestore.get(e.correlationId);
|
||||
if (modelsToRestore) {
|
||||
this.mapCorrelationIdToModelsToRestore.delete(e.correlationId);
|
||||
|
||||
if (textBufferFactory || preferredMode) {
|
||||
restoredModel.updateTextEditorModel(textBufferFactory, preferredMode);
|
||||
}
|
||||
}));
|
||||
}
|
||||
})());
|
||||
await Promise.all(modelsToRestore.map(async modelToRestore => {
|
||||
|
||||
// restore the model, forcing a reload. this is important because
|
||||
// we know the file has changed on disk after the move and the
|
||||
// model might have still existed with the previous state. this
|
||||
// ensures we are not tracking a stale state.
|
||||
const restoredModel = await this.resolve(modelToRestore.target, { reload: { async: false }, encoding: modelToRestore.encoding });
|
||||
|
||||
// restore previous dirty content if any and ensure to mark the model as dirty
|
||||
let textBufferFactory: ITextBufferFactory | undefined = undefined;
|
||||
if (modelToRestore.snapshot) {
|
||||
textBufferFactory = createTextBufferFactoryFromSnapshot(modelToRestore.snapshot);
|
||||
}
|
||||
|
||||
// restore previous mode only if the mode is now unspecified
|
||||
let preferredMode: string | undefined = undefined;
|
||||
if (restoredModel.getMode() === PLAINTEXT_MODE_ID && modelToRestore.mode !== PLAINTEXT_MODE_ID) {
|
||||
preferredMode = modelToRestore.mode;
|
||||
}
|
||||
|
||||
if (textBufferFactory || preferredMode) {
|
||||
restoredModel.updateTextEditorModel(textBufferFactory, preferredMode);
|
||||
}
|
||||
}));
|
||||
}
|
||||
})());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event, IWaitUntil } from 'vs/base/common/event';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IEncodingSupport, IModeSupport, ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor';
|
||||
import { IBaseStatWithMetadata, IFileStatWithMetadata, IReadFileOptions, IWriteFileOptions, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
@@ -21,10 +21,6 @@ import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress';
|
||||
|
||||
export const ITextFileService = createDecorator<ITextFileService>('textFileService');
|
||||
|
||||
export interface TextFileCreateEvent extends IWaitUntil {
|
||||
readonly resource: URI;
|
||||
}
|
||||
|
||||
export interface ITextFileService extends IDisposable {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
@@ -95,11 +91,6 @@ export interface ITextFileService extends IDisposable {
|
||||
*/
|
||||
write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata>;
|
||||
|
||||
/**
|
||||
* An event that is fired after a text file has been created.
|
||||
*/
|
||||
readonly onDidCreateTextFile: Event<TextFileCreateEvent>;
|
||||
|
||||
/**
|
||||
* Create a file. If the file exists it will be overwritten with the contents if
|
||||
* the options enable to overwrite.
|
||||
|
||||
@@ -5,23 +5,18 @@
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService';
|
||||
import { ITextFileService, ITextFileStreamContent, ITextFileContent, IResourceEncodings, IResourceEncoding, IReadTextFileOptions, IWriteTextFileOptions, stringToSnapshot, TextFileOperationResult, TextFileOperationError } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ITextFileService, ITextFileStreamContent, ITextFileContent, IReadTextFileOptions, IWriteTextFileOptions, stringToSnapshot, TextFileOperationResult, TextFileOperationError } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileStatWithMetadata, ICreateFileOptions, FileOperationError, FileOperationResult, IFileStreamContent, IFileService } from 'vs/platform/files/common/files';
|
||||
import { IFileStatWithMetadata, FileOperationError, FileOperationResult, IFileStreamContent, IFileService } from 'vs/platform/files/common/files';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { stat, chmod, MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/base/node/pfs';
|
||||
import { join, dirname } from 'vs/base/common/path';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { UTF8, UTF8_with_bom, UTF16be, UTF16le, encodingExists, UTF8_BOM, toDecodeStream, toEncodeReadable, IDecodeStreamResult, detectEncodingByBOMFromBuffer } from 'vs/base/node/encoding';
|
||||
import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { joinPath, extname, isEqualOrParent } from 'vs/base/common/resources';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { bufferToStream, VSBufferReadable } from 'vs/base/common/buffer';
|
||||
import { UTF8, UTF8_with_bom, toDecodeStream, toEncodeReadable, IDecodeStreamResult } from 'vs/workbench/services/textfile/common/encoding';
|
||||
import { bufferToStream, VSBufferReadable, VSBuffer } from 'vs/base/common/buffer';
|
||||
import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
|
||||
import { ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { consumeStream } from 'vs/base/common/stream';
|
||||
@@ -64,15 +59,6 @@ export class NativeTextFileService extends AbstractTextFileService {
|
||||
super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, dialogService, fileDialogService, textResourceConfigurationService, filesConfigurationService, textModelService, codeEditorService, pathService, workingCopyFileService, uriIdentityService);
|
||||
}
|
||||
|
||||
private _encoding: EncodingOracle | undefined;
|
||||
get encoding(): EncodingOracle {
|
||||
if (!this._encoding) {
|
||||
this._encoding = this._register(this.instantiationService.createInstance(EncodingOracle));
|
||||
}
|
||||
|
||||
return this._encoding;
|
||||
}
|
||||
|
||||
async read(resource: URI, options?: IReadTextFileOptions): Promise<ITextFileContent> {
|
||||
const [bufferStream, decoder] = await this.doRead(resource, {
|
||||
...options,
|
||||
@@ -159,19 +145,20 @@ export class NativeTextFileService extends AbstractTextFileService {
|
||||
return ensuredOptions;
|
||||
}
|
||||
|
||||
protected async doCreate(resource: URI, value?: string, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
|
||||
protected async doEncodeText(resource: URI, value?: string | ITextSnapshot): Promise<VSBuffer | VSBufferReadable | undefined> {
|
||||
|
||||
// check for encoding
|
||||
const { encoding, addBOM } = await this.encoding.getWriteEncoding(resource);
|
||||
|
||||
// return to parent when encoding is standard
|
||||
if (encoding === UTF8 && !addBOM) {
|
||||
return super.doCreate(resource, value, options);
|
||||
return super.doEncodeText(resource, value);
|
||||
}
|
||||
|
||||
// otherwise create with encoding
|
||||
const encodedReadable = await this.getEncodedReadable(value || '', encoding, addBOM);
|
||||
return this.fileService.createFile(resource, encodedReadable, options);
|
||||
|
||||
return encodedReadable;
|
||||
}
|
||||
|
||||
async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
|
||||
@@ -292,151 +279,4 @@ export class NativeTextFileService extends AbstractTextFileService {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IEncodingOverride {
|
||||
parent?: URI;
|
||||
extension?: string;
|
||||
encoding: string;
|
||||
}
|
||||
|
||||
export class EncodingOracle extends Disposable implements IResourceEncodings {
|
||||
|
||||
private _encodingOverrides: IEncodingOverride[];
|
||||
protected get encodingOverrides(): IEncodingOverride[] { return this._encodingOverrides; }
|
||||
protected set encodingOverrides(value: IEncodingOverride[]) { this._encodingOverrides = value; }
|
||||
|
||||
constructor(
|
||||
@ITextResourceConfigurationService private textResourceConfigurationService: ITextResourceConfigurationService,
|
||||
@IEnvironmentService private environmentService: IEnvironmentService,
|
||||
@IWorkspaceContextService private contextService: IWorkspaceContextService,
|
||||
@IFileService private fileService: IFileService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._encodingOverrides = this.getDefaultEncodingOverrides();
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
||||
// Workspace Folder Change
|
||||
this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.encodingOverrides = this.getDefaultEncodingOverrides()));
|
||||
}
|
||||
|
||||
private getDefaultEncodingOverrides(): IEncodingOverride[] {
|
||||
const defaultEncodingOverrides: IEncodingOverride[] = [];
|
||||
|
||||
// Global settings
|
||||
defaultEncodingOverrides.push({ parent: this.environmentService.userRoamingDataHome, encoding: UTF8 });
|
||||
|
||||
// Workspace files (via extension and via untitled workspaces location)
|
||||
defaultEncodingOverrides.push({ extension: WORKSPACE_EXTENSION, encoding: UTF8 });
|
||||
defaultEncodingOverrides.push({ parent: this.environmentService.untitledWorkspacesHome, encoding: UTF8 });
|
||||
|
||||
// Folder Settings
|
||||
this.contextService.getWorkspace().folders.forEach(folder => {
|
||||
defaultEncodingOverrides.push({ parent: joinPath(folder.uri, '.vscode'), encoding: UTF8 });
|
||||
});
|
||||
|
||||
return defaultEncodingOverrides;
|
||||
}
|
||||
|
||||
async getWriteEncoding(resource: URI, options?: IWriteTextFileOptions): Promise<{ encoding: string, addBOM: boolean }> {
|
||||
const { encoding, hasBOM } = await this.getPreferredWriteEncoding(resource, options ? options.encoding : undefined);
|
||||
|
||||
// Some encodings come with a BOM automatically
|
||||
if (hasBOM) {
|
||||
return { encoding, addBOM: true };
|
||||
}
|
||||
|
||||
// Ensure that we preserve an existing BOM if found for UTF8
|
||||
// unless we are instructed to overwrite the encoding
|
||||
const overwriteEncoding = options?.overwriteEncoding;
|
||||
if (!overwriteEncoding && encoding === UTF8) {
|
||||
try {
|
||||
const buffer = (await this.fileService.readFile(resource, { length: UTF8_BOM.length })).value;
|
||||
if (detectEncodingByBOMFromBuffer(buffer, buffer.byteLength) === UTF8_with_bom) {
|
||||
return { encoding, addBOM: true };
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore - file might not exist
|
||||
}
|
||||
}
|
||||
|
||||
return { encoding, addBOM: false };
|
||||
}
|
||||
|
||||
async getPreferredWriteEncoding(resource: URI, preferredEncoding?: string): Promise<IResourceEncoding> {
|
||||
const resourceEncoding = await this.getEncodingForResource(resource, preferredEncoding);
|
||||
|
||||
return {
|
||||
encoding: resourceEncoding,
|
||||
hasBOM: resourceEncoding === UTF16be || resourceEncoding === UTF16le || resourceEncoding === UTF8_with_bom // enforce BOM for certain encodings
|
||||
};
|
||||
}
|
||||
|
||||
getReadEncoding(resource: URI, options: IReadTextFileOptions | undefined, detectedEncoding: string | null): Promise<string> {
|
||||
let preferredEncoding: string | undefined;
|
||||
|
||||
// Encoding passed in as option
|
||||
if (options?.encoding) {
|
||||
if (detectedEncoding === UTF8_with_bom && options.encoding === UTF8) {
|
||||
preferredEncoding = UTF8_with_bom; // indicate the file has BOM if we are to resolve with UTF 8
|
||||
} else {
|
||||
preferredEncoding = options.encoding; // give passed in encoding highest priority
|
||||
}
|
||||
}
|
||||
|
||||
// Encoding detected
|
||||
else if (detectedEncoding) {
|
||||
preferredEncoding = detectedEncoding;
|
||||
}
|
||||
|
||||
// Encoding configured
|
||||
else if (this.textResourceConfigurationService.getValue(resource, 'files.encoding') === UTF8_with_bom) {
|
||||
preferredEncoding = UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then
|
||||
}
|
||||
|
||||
return this.getEncodingForResource(resource, preferredEncoding);
|
||||
}
|
||||
|
||||
private async getEncodingForResource(resource: URI, preferredEncoding?: string): Promise<string> {
|
||||
let fileEncoding: string;
|
||||
|
||||
const override = this.getEncodingOverride(resource);
|
||||
if (override) {
|
||||
fileEncoding = override; // encoding override always wins
|
||||
} else if (preferredEncoding) {
|
||||
fileEncoding = preferredEncoding; // preferred encoding comes second
|
||||
} else {
|
||||
fileEncoding = this.textResourceConfigurationService.getValue(resource, 'files.encoding'); // and last we check for settings
|
||||
}
|
||||
|
||||
if (!fileEncoding || !(await encodingExists(fileEncoding))) {
|
||||
fileEncoding = UTF8; // the default is UTF 8
|
||||
}
|
||||
|
||||
return fileEncoding;
|
||||
}
|
||||
|
||||
private getEncodingOverride(resource: URI): string | undefined {
|
||||
if (this.encodingOverrides && this.encodingOverrides.length) {
|
||||
for (const override of this.encodingOverrides) {
|
||||
|
||||
// check if the resource is child of encoding override path
|
||||
if (override.parent && isEqualOrParent(resource, override.parent)) {
|
||||
return override.encoding;
|
||||
}
|
||||
|
||||
// check if the resource extension is equal to encoding override
|
||||
if (override.extension && extname(resource) === `.${override.extension}`) {
|
||||
return override.encoding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ITextFileService, NativeTextFileService);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { workbenchInstantiationService, TestServiceAccessor, TestTextFileEditorM
|
||||
import { toResource } from 'vs/base/test/common/utils';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { FileOperation } from 'vs/platform/files/common/files';
|
||||
|
||||
suite('Files - TextFileService', () => {
|
||||
|
||||
@@ -100,7 +101,7 @@ suite('Files - TextFileService', () => {
|
||||
assert.ok(!accessor.textFileService.isDirty(model.resource));
|
||||
});
|
||||
|
||||
test('create', async function () {
|
||||
test('create does not overwrite existing model', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model.resource, model);
|
||||
|
||||
@@ -111,14 +112,15 @@ suite('Files - TextFileService', () => {
|
||||
let eventCounter = 0;
|
||||
|
||||
const disposable1 = accessor.workingCopyFileService.addFileOperationParticipant({
|
||||
participate: async target => {
|
||||
assert.equal(target.toString(), model.resource.toString());
|
||||
participate: async files => {
|
||||
assert.equal(files[0].target, model.resource.toString());
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
const disposable2 = accessor.textFileService.onDidCreateTextFile(e => {
|
||||
assert.equal(e.resource.toString(), model.resource.toString());
|
||||
const disposable2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
assert.equal(e.operation, FileOperation.CREATE);
|
||||
assert.equal(e.files[0].target.toString(), model.resource.toString());
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ITextFileService, snapshotToString, TextFileOperationResult, TextFileOperationError } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ITextFileService, snapshotToString, TextFileOperationResult, TextFileOperationError, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
@@ -20,13 +20,16 @@ import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemPro
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { join, basename } from 'vs/base/common/path';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { UTF16be, UTF16le, UTF8_with_bom, UTF8 } from 'vs/base/node/encoding';
|
||||
import { UTF16be, UTF16le, UTF8_with_bom, UTF8 } from 'vs/workbench/services/textfile/common/encoding';
|
||||
import { DefaultEndOfLine, ITextSnapshot } from 'vs/editor/common/model';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { readFileSync, statSync } from 'fs';
|
||||
import { detectEncodingByBOM } from 'vs/base/test/node/encoding/encoding.test';
|
||||
import { detectEncodingByBOM } from 'vs/workbench/services/textfile/test/node/encoding/encoding.test';
|
||||
import { workbenchInstantiationService, TestNativeTextFileServiceWithEncodingOverrides } from 'vs/workbench/test/electron-browser/workbenchTestServices';
|
||||
import { WorkingCopyFileService, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { TestWorkingCopyService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService';
|
||||
|
||||
suite('Files - TextFileService i/o', function () {
|
||||
const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'textfileservice');
|
||||
@@ -57,6 +60,8 @@ suite('Files - TextFileService i/o', function () {
|
||||
const collection = new ServiceCollection();
|
||||
collection.set(IFileService, fileService);
|
||||
|
||||
collection.set(IWorkingCopyFileService, new WorkingCopyFileService(fileService, new TestWorkingCopyService(), instantiationService, new UriIdentityService(fileService)));
|
||||
|
||||
service = instantiationService.createChild(collection).createInstance(TestNativeTextFileServiceWithEncodingOverrides);
|
||||
|
||||
const id = generateUuid();
|
||||
@@ -82,7 +87,7 @@ suite('Files - TextFileService i/o', function () {
|
||||
assert.equal(await exists(resource.fsPath), true);
|
||||
});
|
||||
|
||||
test('create - no encoding - content provided', async () => {
|
||||
test('create - no encoding - content provided (string)', async () => {
|
||||
const resource = URI.file(join(testDir, 'small_new.txt'));
|
||||
|
||||
await service.create(resource, 'Hello World');
|
||||
@@ -91,6 +96,15 @@ suite('Files - TextFileService i/o', function () {
|
||||
assert.equal((await readFile(resource.fsPath)).toString(), 'Hello World');
|
||||
});
|
||||
|
||||
test('create - no encoding - content provided (snapshot)', async () => {
|
||||
const resource = URI.file(join(testDir, 'small_new.txt'));
|
||||
|
||||
await service.create(resource, stringToSnapshot('Hello World'));
|
||||
|
||||
assert.equal(await exists(resource.fsPath), true);
|
||||
assert.equal((await readFile(resource.fsPath)).toString(), 'Hello World');
|
||||
});
|
||||
|
||||
test('create - UTF 16 LE - no content', async () => {
|
||||
const resource = URI.file(join(testDir, 'small_new.utf16le'));
|
||||
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as fs from 'fs';
|
||||
import * as encoding from 'vs/workbench/services/textfile/common/encoding';
|
||||
import * as terminalEncoding from 'vs/base/node/terminalEncoding';
|
||||
import * as streams from 'vs/base/common/stream';
|
||||
import * as iconv from 'iconv-lite-umd';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { newWriteableBufferStream, VSBuffer, VSBufferReadableStream, streamToBufferReadableStream } from 'vs/base/common/buffer';
|
||||
|
||||
export async function detectEncodingByBOM(file: string): Promise<typeof encoding.UTF16be | typeof encoding.UTF16le | typeof encoding.UTF8_with_bom | null> {
|
||||
try {
|
||||
const { buffer, bytesRead } = await readExactlyByFile(file, 3);
|
||||
|
||||
return encoding.detectEncodingByBOMFromBuffer(buffer, bytesRead);
|
||||
} catch (error) {
|
||||
return null; // ignore errors (like file not found)
|
||||
}
|
||||
}
|
||||
|
||||
interface ReadResult {
|
||||
buffer: VSBuffer | null;
|
||||
bytesRead: number;
|
||||
}
|
||||
|
||||
function readExactlyByFile(file: string, totalBytes: number): Promise<ReadResult> {
|
||||
return new Promise<ReadResult>((resolve, reject) => {
|
||||
fs.open(file, 'r', null, (err, fd) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
function end(err: Error | null, resultBuffer: Buffer | null, bytesRead: number): void {
|
||||
fs.close(fd, closeError => {
|
||||
if (closeError) {
|
||||
return reject(closeError);
|
||||
}
|
||||
|
||||
if (err && (<any>err).code === 'EISDIR') {
|
||||
return reject(err); // we want to bubble this error up (file is actually a folder)
|
||||
}
|
||||
|
||||
return resolve({ buffer: resultBuffer ? VSBuffer.wrap(resultBuffer) : null, bytesRead });
|
||||
});
|
||||
}
|
||||
|
||||
const buffer = Buffer.allocUnsafe(totalBytes);
|
||||
let offset = 0;
|
||||
|
||||
function readChunk(): void {
|
||||
fs.read(fd, buffer, offset, totalBytes - offset, null, (err, bytesRead) => {
|
||||
if (err) {
|
||||
return end(err, null, 0);
|
||||
}
|
||||
|
||||
if (bytesRead === 0) {
|
||||
return end(null, buffer, offset);
|
||||
}
|
||||
|
||||
offset += bytesRead;
|
||||
|
||||
if (offset === totalBytes) {
|
||||
return end(null, buffer, offset);
|
||||
}
|
||||
|
||||
return readChunk();
|
||||
});
|
||||
}
|
||||
|
||||
readChunk();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
suite('Encoding', () => {
|
||||
|
||||
test('detectBOM does not return error for non existing file', async () => {
|
||||
const file = getPathFromAmdModule(require, './fixtures/not-exist.css');
|
||||
|
||||
const detectedEncoding = await detectEncodingByBOM(file);
|
||||
assert.equal(detectedEncoding, null);
|
||||
});
|
||||
|
||||
test('detectBOM UTF-8', async () => {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some_utf8.css');
|
||||
|
||||
const detectedEncoding = await detectEncodingByBOM(file);
|
||||
assert.equal(detectedEncoding, 'utf8bom');
|
||||
});
|
||||
|
||||
test('detectBOM UTF-16 LE', async () => {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some_utf16le.css');
|
||||
|
||||
const detectedEncoding = await detectEncodingByBOM(file);
|
||||
assert.equal(detectedEncoding, 'utf16le');
|
||||
});
|
||||
|
||||
test('detectBOM UTF-16 BE', async () => {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some_utf16be.css');
|
||||
|
||||
const detectedEncoding = await detectEncodingByBOM(file);
|
||||
assert.equal(detectedEncoding, 'utf16be');
|
||||
});
|
||||
|
||||
test('detectBOM ANSI', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some_ansi.css');
|
||||
|
||||
const detectedEncoding = await detectEncodingByBOM(file);
|
||||
assert.equal(detectedEncoding, null);
|
||||
});
|
||||
|
||||
test('detectBOM ANSI', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/empty.txt');
|
||||
|
||||
const detectedEncoding = await detectEncodingByBOM(file);
|
||||
assert.equal(detectedEncoding, null);
|
||||
});
|
||||
|
||||
test('resolve terminal encoding (detect)', async function () {
|
||||
const enc = await terminalEncoding.resolveTerminalEncoding();
|
||||
assert.ok(enc.length > 0);
|
||||
});
|
||||
|
||||
test('resolve terminal encoding (environment)', async function () {
|
||||
process.env['VSCODE_CLI_ENCODING'] = 'utf16le';
|
||||
|
||||
const enc = await terminalEncoding.resolveTerminalEncoding();
|
||||
assert.ok(await encoding.encodingExists(enc));
|
||||
assert.equal(enc, 'utf16le');
|
||||
});
|
||||
|
||||
test('detectEncodingFromBuffer (JSON saved as PNG)', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some.json.png');
|
||||
|
||||
const buffer = await readExactlyByFile(file, 512);
|
||||
const mimes = encoding.detectEncodingFromBuffer(buffer);
|
||||
assert.equal(mimes.seemsBinary, false);
|
||||
});
|
||||
|
||||
test('detectEncodingFromBuffer (PNG saved as TXT)', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some.png.txt');
|
||||
const buffer = await readExactlyByFile(file, 512);
|
||||
const mimes = encoding.detectEncodingFromBuffer(buffer);
|
||||
assert.equal(mimes.seemsBinary, true);
|
||||
});
|
||||
|
||||
test('detectEncodingFromBuffer (XML saved as PNG)', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some.xml.png');
|
||||
const buffer = await readExactlyByFile(file, 512);
|
||||
const mimes = encoding.detectEncodingFromBuffer(buffer);
|
||||
assert.equal(mimes.seemsBinary, false);
|
||||
});
|
||||
|
||||
test('detectEncodingFromBuffer (QWOFF saved as TXT)', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some.qwoff.txt');
|
||||
const buffer = await readExactlyByFile(file, 512);
|
||||
const mimes = encoding.detectEncodingFromBuffer(buffer);
|
||||
assert.equal(mimes.seemsBinary, true);
|
||||
});
|
||||
|
||||
test('detectEncodingFromBuffer (CSS saved as QWOFF)', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some.css.qwoff');
|
||||
const buffer = await readExactlyByFile(file, 512);
|
||||
const mimes = encoding.detectEncodingFromBuffer(buffer);
|
||||
assert.equal(mimes.seemsBinary, false);
|
||||
});
|
||||
|
||||
test('detectEncodingFromBuffer (PDF)', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some.pdf');
|
||||
const buffer = await readExactlyByFile(file, 512);
|
||||
const mimes = encoding.detectEncodingFromBuffer(buffer);
|
||||
assert.equal(mimes.seemsBinary, true);
|
||||
});
|
||||
|
||||
test('detectEncodingFromBuffer (guess UTF-16 LE from content without BOM)', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/utf16_le_nobom.txt');
|
||||
const buffer = await readExactlyByFile(file, 512);
|
||||
const mimes = encoding.detectEncodingFromBuffer(buffer);
|
||||
assert.equal(mimes.encoding, encoding.UTF16le);
|
||||
assert.equal(mimes.seemsBinary, false);
|
||||
});
|
||||
|
||||
test('detectEncodingFromBuffer (guess UTF-16 BE from content without BOM)', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/utf16_be_nobom.txt');
|
||||
const buffer = await readExactlyByFile(file, 512);
|
||||
const mimes = encoding.detectEncodingFromBuffer(buffer);
|
||||
assert.equal(mimes.encoding, encoding.UTF16be);
|
||||
assert.equal(mimes.seemsBinary, false);
|
||||
});
|
||||
|
||||
test('autoGuessEncoding (UTF8)', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some_file.css');
|
||||
const buffer = await readExactlyByFile(file, 512 * 8);
|
||||
const mimes = await encoding.detectEncodingFromBuffer(buffer, true);
|
||||
assert.equal(mimes.encoding, 'utf8');
|
||||
});
|
||||
|
||||
test('autoGuessEncoding (ASCII)', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some_ansi.css');
|
||||
const buffer = await readExactlyByFile(file, 512 * 8);
|
||||
const mimes = await encoding.detectEncodingFromBuffer(buffer, true);
|
||||
assert.equal(mimes.encoding, null);
|
||||
});
|
||||
|
||||
test('autoGuessEncoding (ShiftJIS)', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some.shiftjis.txt');
|
||||
const buffer = await readExactlyByFile(file, 512 * 8);
|
||||
const mimes = await encoding.detectEncodingFromBuffer(buffer, true);
|
||||
assert.equal(mimes.encoding, 'shiftjis');
|
||||
});
|
||||
|
||||
test('autoGuessEncoding (CP1252)', async function () {
|
||||
const file = getPathFromAmdModule(require, './fixtures/some.cp1252.txt');
|
||||
const buffer = await readExactlyByFile(file, 512 * 8);
|
||||
const mimes = await encoding.detectEncodingFromBuffer(buffer, true);
|
||||
assert.equal(mimes.encoding, 'windows1252');
|
||||
});
|
||||
|
||||
async function readAndDecodeFromDisk(path: string, fileEncoding: string | null) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
fs.readFile(path, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(iconv.decode(data, encoding.toNodeEncoding(fileEncoding!)));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function newTestReadableStream(buffers: Buffer[]): VSBufferReadableStream {
|
||||
const stream = newWriteableBufferStream();
|
||||
buffers
|
||||
.map(VSBuffer.wrap)
|
||||
.forEach(buffer => {
|
||||
setTimeout(() => {
|
||||
stream.write(buffer);
|
||||
});
|
||||
});
|
||||
setTimeout(() => {
|
||||
stream.end();
|
||||
});
|
||||
return stream;
|
||||
}
|
||||
|
||||
async function readAllAsString(stream: streams.ReadableStream<string>) {
|
||||
return streams.consumeStream(stream, strings => strings.join(''));
|
||||
}
|
||||
|
||||
test('toDecodeStream - some stream', async function () {
|
||||
const source = newTestReadableStream([
|
||||
Buffer.from([65, 66, 67]),
|
||||
Buffer.from([65, 66, 67]),
|
||||
Buffer.from([65, 66, 67]),
|
||||
]);
|
||||
|
||||
const { detected, stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 4, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 });
|
||||
|
||||
assert.ok(detected);
|
||||
assert.ok(stream);
|
||||
|
||||
const content = await readAllAsString(stream);
|
||||
assert.equal(content, 'ABCABCABC');
|
||||
});
|
||||
|
||||
test('toDecodeStream - some stream, expect too much data', async function () {
|
||||
const source = newTestReadableStream([
|
||||
Buffer.from([65, 66, 67]),
|
||||
Buffer.from([65, 66, 67]),
|
||||
Buffer.from([65, 66, 67]),
|
||||
]);
|
||||
|
||||
const { detected, stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 64, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 });
|
||||
|
||||
assert.ok(detected);
|
||||
assert.ok(stream);
|
||||
|
||||
const content = await readAllAsString(stream);
|
||||
assert.equal(content, 'ABCABCABC');
|
||||
});
|
||||
|
||||
test('toDecodeStream - some stream, no data', async function () {
|
||||
const source = newWriteableBufferStream();
|
||||
source.end();
|
||||
|
||||
const { detected, stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 512, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 });
|
||||
|
||||
assert.ok(detected);
|
||||
assert.ok(stream);
|
||||
|
||||
const content = await readAllAsString(stream);
|
||||
assert.equal(content, '');
|
||||
});
|
||||
|
||||
|
||||
test('toDecodeStream - encoding, utf16be', async function () {
|
||||
const path = getPathFromAmdModule(require, './fixtures/some_utf16be.css');
|
||||
const source = streamToBufferReadableStream(fs.createReadStream(path));
|
||||
|
||||
const { detected, stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 64, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 });
|
||||
|
||||
assert.equal(detected.encoding, 'utf16be');
|
||||
assert.equal(detected.seemsBinary, false);
|
||||
|
||||
const expected = await readAndDecodeFromDisk(path, detected.encoding);
|
||||
const actual = await readAllAsString(stream);
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
||||
|
||||
test('toDecodeStream - empty file', async function () {
|
||||
const path = getPathFromAmdModule(require, './fixtures/empty.txt');
|
||||
const source = streamToBufferReadableStream(fs.createReadStream(path));
|
||||
const { detected, stream } = await encoding.toDecodeStream(source, { guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 });
|
||||
|
||||
const expected = await readAndDecodeFromDisk(path, detected.encoding);
|
||||
const actual = await readAllAsString(stream);
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
||||
test('toDecodeStream - decodes buffer entirely', async function () {
|
||||
const emojis = Buffer.from('🖥️💻💾');
|
||||
const incompleteEmojis = emojis.slice(0, emojis.length - 1);
|
||||
|
||||
const buffers: Buffer[] = [];
|
||||
for (let i = 0; i < incompleteEmojis.length; i++) {
|
||||
buffers.push(incompleteEmojis.slice(i, i + 1));
|
||||
}
|
||||
|
||||
const source = newTestReadableStream(buffers);
|
||||
const { stream } = await encoding.toDecodeStream(source, { minBytesRequiredForDetection: 4, guessEncoding: false, overwriteEncoding: async detected => detected || encoding.UTF8 });
|
||||
|
||||
const expected = incompleteEmojis.toString(encoding.UTF8);
|
||||
const actual = await readAllAsString(stream);
|
||||
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
||||
test('toEncodeReadable - encoding, utf16be', async function () {
|
||||
const path = getPathFromAmdModule(require, './fixtures/some_utf16be.css');
|
||||
const source = await readAndDecodeFromDisk(path, encoding.UTF16be);
|
||||
|
||||
const expected = VSBuffer.wrap(
|
||||
iconv.encode(source, encoding.toNodeEncoding(encoding.UTF16be))
|
||||
).toString();
|
||||
|
||||
const actual = streams.consumeReadable(
|
||||
await encoding.toEncodeReadable(streams.toReadable(source), encoding.UTF16be),
|
||||
VSBuffer.concat
|
||||
).toString();
|
||||
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
||||
test('toEncodeReadable - empty readable to utf8', async function () {
|
||||
const source: streams.Readable<string> = {
|
||||
read() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const actual = streams.consumeReadable(
|
||||
await encoding.toEncodeReadable(source, encoding.UTF8),
|
||||
VSBuffer.concat
|
||||
).toString();
|
||||
|
||||
assert.equal(actual, '');
|
||||
});
|
||||
|
||||
[{
|
||||
utfEncoding: encoding.UTF8,
|
||||
relatedBom: encoding.UTF8_BOM
|
||||
}, {
|
||||
utfEncoding: encoding.UTF8_with_bom,
|
||||
relatedBom: encoding.UTF8_BOM
|
||||
}, {
|
||||
utfEncoding: encoding.UTF16be,
|
||||
relatedBom: encoding.UTF16be_BOM,
|
||||
}, {
|
||||
utfEncoding: encoding.UTF16le,
|
||||
relatedBom: encoding.UTF16le_BOM
|
||||
}].forEach(({ utfEncoding, relatedBom }) => {
|
||||
test(`toEncodeReadable - empty readable to ${utfEncoding} with BOM`, async function () {
|
||||
const source: streams.Readable<string> = {
|
||||
read() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const encodedReadable = encoding.toEncodeReadable(source, utfEncoding, { addBOM: true });
|
||||
|
||||
const expected = VSBuffer.wrap(Buffer.from(relatedBom)).toString();
|
||||
const actual = streams.consumeReadable(await encodedReadable, VSBuffer.concat).toString();
|
||||
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Collections;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
using System.Data;
|
||||
using System.Data.OleDb;
|
||||
using System.Data.Odbc;
|
||||
using System.IO;
|
||||
using System.Net.Mail;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.DirectoryServices;
|
||||
using System.Diagnostics;
|
||||
using System.Resources;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
|
||||
ObjectCount = LoadObjects("Öffentlicher Ordner");
|
||||
|
||||
Private = "Persönliche Information"
|
||||
@@ -0,0 +1,35 @@
|
||||
/*----------------------------------------------------------
|
||||
The base color for this template is #5c87b2. If you'd like
|
||||
to use a different color start by replacing all instances of
|
||||
#5c87b2 with your new color.
|
||||
----------------------------------------------------------*/
|
||||
body
|
||||
{
|
||||
background-color: #5c87b2;
|
||||
font-size: .75em;
|
||||
font-family: Segoe UI, Verdana, Helvetica, Sans-Serif;
|
||||
margin: 8px;
|
||||
padding: 0;
|
||||
color: #696969;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6
|
||||
{
|
||||
color: #000;
|
||||
font-size: 40px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
textarea
|
||||
{
|
||||
font-family: Consolas
|
||||
}
|
||||
|
||||
#results
|
||||
{
|
||||
margin-top: 2em;
|
||||
margin-left: 2em;
|
||||
color: black;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "typescript",
|
||||
|
||||
"sources": [
|
||||
"examples/company.ts",
|
||||
"examples/conway.ts",
|
||||
"examples/employee.ts",
|
||||
"examples/large.ts",
|
||||
"examples/small.ts"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 151 B |
Binary file not shown.
@@ -0,0 +1 @@
|
||||
VSCODEは最高のエディタだ。
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml>
|
||||
|
||||
</xml>
|
||||
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/*----------------------------------------------------------
|
||||
The base color for this template is #5c87b2. If you'd like
|
||||
to use a different color start by replacing all instances of
|
||||
#5c87b2 with your new color.
|
||||
----------------------------------------------------------*/
|
||||
body
|
||||
{
|
||||
background-color: #5c87b2;
|
||||
font-size: .75em;
|
||||
font-family: Segoe UI, Verdana, Helvetica, Sans-Serif;
|
||||
margin: 8px;
|
||||
padding: 0;
|
||||
color: #696969;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6
|
||||
{
|
||||
color: #000;
|
||||
font-size: 40px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
textarea
|
||||
{
|
||||
font-family: Consolas
|
||||
}
|
||||
|
||||
#results
|
||||
{
|
||||
margin-top: 2em;
|
||||
margin-left: 2em;
|
||||
color: black;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/*----------------------------------------------------------
|
||||
The base color for this template is #5c87b2. If you'd like
|
||||
to use a different color start by replacing all instances of
|
||||
#5c87b2 with your new color.
|
||||
|
||||
öäüßßß
|
||||
----------------------------------------------------------*/
|
||||
body
|
||||
{
|
||||
background-color: #5c87b2;
|
||||
font-size: .75em;
|
||||
font-family: Segoe UI, Verdana, Helvetica, Sans-Serif;
|
||||
margin: 8px;
|
||||
padding: 0;
|
||||
color: #696969;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6
|
||||
{
|
||||
color: #000;
|
||||
font-size: 40px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
textarea
|
||||
{
|
||||
font-family: Consolas
|
||||
}
|
||||
|
||||
#results
|
||||
{
|
||||
margin-top: 2em;
|
||||
margin-left: 2em;
|
||||
color: black;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,42 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/*----------------------------------------------------------
|
||||
The base color for this template is #5c87b2. If you'd like
|
||||
to use a different color start by replacing all instances of
|
||||
#5c87b2 with your new color.
|
||||
|
||||
öäüßßß
|
||||
----------------------------------------------------------*/
|
||||
body
|
||||
{
|
||||
background-color: #5c87b2;
|
||||
font-size: .75em;
|
||||
font-family: Segoe UI, Verdana, Helvetica, Sans-Serif;
|
||||
margin: 8px;
|
||||
padding: 0;
|
||||
color: #696969;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6
|
||||
{
|
||||
color: #000;
|
||||
font-size: 40px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
textarea
|
||||
{
|
||||
font-family: Consolas
|
||||
}
|
||||
|
||||
#results
|
||||
{
|
||||
margin-top: 2em;
|
||||
margin-left: 2em;
|
||||
color: black;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -33,7 +33,11 @@ class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorMod
|
||||
super();
|
||||
}
|
||||
|
||||
async createReferencedObject(key: string, skipActivateProvider?: boolean): Promise<ITextEditorModel> {
|
||||
createReferencedObject(key: string): Promise<ITextEditorModel> {
|
||||
return this.doCreateReferencedObject(key);
|
||||
}
|
||||
|
||||
private async doCreateReferencedObject(key: string, skipActivateProvider?: boolean): Promise<ITextEditorModel> {
|
||||
|
||||
// Untrack as being disposed
|
||||
this.modelsToDispose.delete(key);
|
||||
@@ -70,7 +74,7 @@ class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorMod
|
||||
if (!skipActivateProvider) {
|
||||
await this.fileService.activateProvider(resource.scheme);
|
||||
|
||||
return this.createReferencedObject(key, true);
|
||||
return this.doCreateReferencedObject(key, true);
|
||||
}
|
||||
|
||||
throw new Error(`Unable to resolve resource ${key}`);
|
||||
@@ -179,6 +183,7 @@ export class TextModelResolverService extends Disposable implements ITextModelSe
|
||||
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(new ModelUndoRedoParticipant(this.modelService, this, this.undoRedoService));
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
|
||||
import { extUri } from 'vs/base/common/resources';
|
||||
|
||||
/**
|
||||
* An editor input to be used for untitled text buffers.
|
||||
@@ -122,13 +123,12 @@ export class UntitledTextEditorInput extends AbstractTextResourceEditorInput imp
|
||||
}
|
||||
|
||||
matches(otherInput: unknown): boolean {
|
||||
if (super.matches(otherInput) === true) {
|
||||
if (otherInput === this) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise compare by properties
|
||||
if (otherInput instanceof UntitledTextEditorInput) {
|
||||
return otherInput.resource.toString() === this.resource.toString();
|
||||
return extUri.isEqual(otherInput.resource, this.resource);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { IUserDataSyncService, IAuthenticationProvider, getUserDataSyncStore, isAuthenticationProvider, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNCED_DATA_COMMAND_ID } from 'vs/workbench/services/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE, SHOW_SYNCED_DATA_COMMAND_ID, SHOW_SYNC_LOG_COMMAND_ID, getSyncAreaLabel } from 'vs/workbench/services/userDataSync/common/userDataSync';
|
||||
import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
@@ -28,6 +28,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
|
||||
|
||||
type UserAccountClassification = {
|
||||
id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' };
|
||||
@@ -90,6 +91,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
@IExtensionService extensionService: IExtensionService,
|
||||
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IProgressService private readonly progressService: IProgressService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@@ -219,18 +221,34 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat
|
||||
throw new Error(localize('no account', "No account available"));
|
||||
}
|
||||
|
||||
const pullFirst = await this.handleFirstTimeSync();
|
||||
await this.userDataAutoSyncService.turnOn(pullFirst);
|
||||
this.notificationService.info(localize('sync turned on', "Preferences sync is turned on"));
|
||||
const preferencesSyncTitle = localize('preferences sync', "Preferences Sync");
|
||||
const title = `${preferencesSyncTitle} [(${localize('details', "details")})](command:${SHOW_SYNC_LOG_COMMAND_ID})`;
|
||||
await this.progressService.withProgress({
|
||||
location: ProgressLocation.Notification,
|
||||
title,
|
||||
delay: 500,
|
||||
}, async (progress) => {
|
||||
progress.report({ message: localize('turning on', "Turning on...") });
|
||||
const pullFirst = await this.isSyncingWithAnotherMachine();
|
||||
const disposable = this.userDataSyncService.onSynchronizeResource(resource =>
|
||||
progress.report({ message: localize('syncing resource', "Syncing {0}...", getSyncAreaLabel(resource)) }));
|
||||
try {
|
||||
await this.userDataAutoSyncService.turnOn(pullFirst);
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
this.notificationService.info(localize('sync turned on', "{0} is turned on", title));
|
||||
}
|
||||
|
||||
turnoff(everywhere: boolean): Promise<void> {
|
||||
return this.userDataAutoSyncService.turnOff(everywhere);
|
||||
}
|
||||
|
||||
private async handleFirstTimeSync(): Promise<boolean> {
|
||||
const isFirstTimeSyncingWithAnotherMachine = await this.userDataSyncService.isFirstTimeSyncingWithAnotherMachine();
|
||||
if (!isFirstTimeSyncingWithAnotherMachine) {
|
||||
private async isSyncingWithAnotherMachine(): Promise<boolean> {
|
||||
const isSyncingWithAnotherMachine = await this.userDataSyncService.isFirstTimeSyncingWithAnotherMachine();
|
||||
if (!isSyncingWithAnotherMachine) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
private _onSyncErrors: Emitter<[SyncResource, UserDataSyncError][]> = this._register(new Emitter<[SyncResource, UserDataSyncError][]>());
|
||||
readonly onSyncErrors: Event<[SyncResource, UserDataSyncError][]> = this._onSyncErrors.event;
|
||||
|
||||
get onSynchronizeResource(): Event<SyncResource> { return this.channel.listen<SyncResource>('onSynchronizeResource'); }
|
||||
|
||||
constructor(
|
||||
@ISharedProcessService sharedProcessService: ISharedProcessService
|
||||
) {
|
||||
|
||||
@@ -33,7 +33,7 @@ export class WorkingCopyFileOperationParticipant extends Disposable {
|
||||
return toDisposable(() => remove());
|
||||
}
|
||||
|
||||
async participate(target: URI, source: URI | undefined, operation: FileOperation): Promise<void> {
|
||||
async participate(files: { source?: URI, target: URI }[], operation: FileOperation): Promise<void> {
|
||||
const timeout = this.configurationService.getValue<number>('files.participants.timeout');
|
||||
if (timeout <= 0) {
|
||||
return; // disabled
|
||||
@@ -53,7 +53,7 @@ export class WorkingCopyFileOperationParticipant extends Disposable {
|
||||
}
|
||||
|
||||
try {
|
||||
const promise = participant.participate(target, source, operation, progress, timeout, cts.token);
|
||||
const promise = participant.participate(files, operation, progress, timeout, cts.token);
|
||||
await raceTimeout(promise, timeout, () => cts.dispose(true /* cancel */));
|
||||
} catch (err) {
|
||||
this.logService.warn(err);
|
||||
|
||||
@@ -15,9 +15,23 @@ import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/working
|
||||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress';
|
||||
import { WorkingCopyFileOperationParticipant } from 'vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant';
|
||||
import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
|
||||
export const IWorkingCopyFileService = createDecorator<IWorkingCopyFileService>('workingCopyFileService');
|
||||
|
||||
interface SourceTargetPair {
|
||||
|
||||
/**
|
||||
* The source resource that is defined for move operations.
|
||||
*/
|
||||
readonly source?: URI;
|
||||
|
||||
/**
|
||||
* The target resource the event is about.
|
||||
*/
|
||||
readonly target: URI
|
||||
}
|
||||
|
||||
export interface WorkingCopyFileEvent extends IWaitUntil {
|
||||
|
||||
/**
|
||||
@@ -32,25 +46,19 @@ export interface WorkingCopyFileEvent extends IWaitUntil {
|
||||
readonly operation: FileOperation;
|
||||
|
||||
/**
|
||||
* The resource the event is about.
|
||||
* The array of source/target pair of files involved in given operation.
|
||||
*/
|
||||
readonly target: URI;
|
||||
|
||||
/**
|
||||
* A property that is defined for move operations.
|
||||
*/
|
||||
readonly source?: URI;
|
||||
readonly files: SourceTargetPair[]
|
||||
}
|
||||
|
||||
export interface IWorkingCopyFileOperationParticipant {
|
||||
|
||||
/**
|
||||
* Participate in a file operation of a working copy. Allows to
|
||||
* change the working copy before it is being saved to disk.
|
||||
* Participate in a file operation of working copies. Allows to
|
||||
* change the working copies before they are being saved to disk.
|
||||
*/
|
||||
participate(
|
||||
target: URI,
|
||||
source: URI | undefined,
|
||||
files: SourceTargetPair[],
|
||||
operation: FileOperation,
|
||||
progress: IProgress<IProgressStep>,
|
||||
timeout: number,
|
||||
@@ -111,43 +119,49 @@ export interface IWorkingCopyFileService {
|
||||
*/
|
||||
addFileOperationParticipant(participant: IWorkingCopyFileOperationParticipant): IDisposable;
|
||||
|
||||
/**
|
||||
* Execute all known file operation participants.
|
||||
*/
|
||||
runFileOperationParticipants(target: URI, source: URI | undefined, operation: FileOperation): Promise<void>
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region File operations
|
||||
|
||||
/**
|
||||
* Will move working copies matching the provided resource and children
|
||||
* to the target resource using the associated file service for that resource.
|
||||
* Will create a resource with the provided optional contents, optionally overwriting any target.
|
||||
*
|
||||
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
|
||||
* `onDidRunWorkingCopyFileOperation` events to participate.
|
||||
*/
|
||||
move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata>;
|
||||
create(resource: URI, contents?: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata>;
|
||||
|
||||
/**
|
||||
* Will copy working copies matching the provided resource and children
|
||||
* to the target using the associated file service for that resource.
|
||||
* Will move working copies matching the provided resources and corresponding children
|
||||
* to the target resources using the associated file service for those resources.
|
||||
*
|
||||
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
|
||||
* `onDidRunWorkingCopyFileOperation` events to participate.
|
||||
*/
|
||||
copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata>;
|
||||
move(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]>;
|
||||
|
||||
/**
|
||||
* Will delete working copies matching the provided resource and children
|
||||
* using the associated file service for that resource.
|
||||
* Will copy working copies matching the provided resources and corresponding children
|
||||
* to the target resources using the associated file service for those resources.
|
||||
*
|
||||
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
|
||||
* `onDidRunWorkingCopyFileOperation` events to participate.
|
||||
*/
|
||||
delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void>;
|
||||
copy(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]>;
|
||||
|
||||
/**
|
||||
* Will delete working copies matching the provided resources and children
|
||||
* using the associated file service for those resources.
|
||||
*
|
||||
* Working copy owners can listen to the `onWillRunWorkingCopyFileOperation` and
|
||||
* `onDidRunWorkingCopyFileOperation` events to participate.
|
||||
*/
|
||||
delete(resources: URI[], options?: { useTrash?: boolean, recursive?: boolean }): Promise<void>;
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Path related
|
||||
|
||||
/**
|
||||
@@ -209,55 +223,28 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
|
||||
});
|
||||
}
|
||||
|
||||
async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
|
||||
return this.moveOrCopy(source, target, true, overwrite);
|
||||
}
|
||||
|
||||
async copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
|
||||
return this.moveOrCopy(source, target, false, overwrite);
|
||||
}
|
||||
//#region File operations
|
||||
|
||||
private async moveOrCopy(source: URI, target: URI, move: boolean, overwrite?: boolean): Promise<IFileStatWithMetadata> {
|
||||
async create(resource: URI, contents?: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata> {
|
||||
|
||||
// validate move/copy operation before starting
|
||||
const validateMoveOrCopy = await (move ? this.fileService.canMove(source, target, overwrite) : this.fileService.canCopy(source, target, overwrite));
|
||||
if (validateMoveOrCopy instanceof Error) {
|
||||
throw validateMoveOrCopy;
|
||||
// validate create operation before starting
|
||||
const validateCreate = await this.fileService.canCreateFile(resource, options);
|
||||
if (validateCreate instanceof Error) {
|
||||
throw validateCreate;
|
||||
}
|
||||
|
||||
// file operation participant
|
||||
await this.runFileOperationParticipants(target, source, move ? FileOperation.MOVE : FileOperation.COPY);
|
||||
await this.runFileOperationParticipants([{ target: resource }], FileOperation.CREATE);
|
||||
|
||||
// Before doing the heave operations, check first if source and target
|
||||
// are either identical or are considered to be identical for the file
|
||||
// system. In that case we want the model to stay as is and only do the
|
||||
// raw file operation.
|
||||
if (this.uriIdentityService.extUri.isEqual(source, target)) {
|
||||
if (move) {
|
||||
return this.fileService.move(source, target, overwrite);
|
||||
} else {
|
||||
return this.fileService.copy(source, target, overwrite);
|
||||
}
|
||||
}
|
||||
|
||||
// before event
|
||||
const event = { correlationId: this.correlationIds++, operation: move ? FileOperation.MOVE : FileOperation.COPY, target, source };
|
||||
// before events
|
||||
const event = { correlationId: this.correlationIds++, operation: FileOperation.CREATE, files: [{ target: resource }] };
|
||||
await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
// handle dirty working copies depending on the operation:
|
||||
// - move: revert both source and target (if any)
|
||||
// - copy: revert target (if any)
|
||||
const dirtyWorkingCopies = (move ? [...this.getDirty(source), ...this.getDirty(target)] : this.getDirty(target));
|
||||
await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true })));
|
||||
|
||||
// now we can rename the source to target via file operation
|
||||
// now actually create on disk
|
||||
let stat: IFileStatWithMetadata;
|
||||
try {
|
||||
if (move) {
|
||||
stat = await this.fileService.move(source, target, overwrite);
|
||||
} else {
|
||||
stat = await this.fileService.copy(source, target, overwrite);
|
||||
}
|
||||
stat = await this.fileService.createFile(resource, contents, { overwrite: options?.overwrite });
|
||||
} catch (error) {
|
||||
|
||||
// error event
|
||||
@@ -272,30 +259,97 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
|
||||
return stat;
|
||||
}
|
||||
|
||||
async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {
|
||||
async move(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]> {
|
||||
return this.doMoveOrCopy(files, true, options);
|
||||
}
|
||||
|
||||
// validate delete operation before starting
|
||||
const validateDelete = await this.fileService.canDelete(resource, options);
|
||||
if (validateDelete instanceof Error) {
|
||||
throw validateDelete;
|
||||
async copy(files: Required<SourceTargetPair>[], options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]> {
|
||||
return this.doMoveOrCopy(files, false, options);
|
||||
}
|
||||
|
||||
private async doMoveOrCopy(files: Required<SourceTargetPair>[], move: boolean, options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata[]> {
|
||||
const overwrite = options?.overwrite;
|
||||
const stats: IFileStatWithMetadata[] = [];
|
||||
|
||||
// validate move/copy operation before starting
|
||||
for (const { source, target } of files) {
|
||||
const validateMoveOrCopy = await (move ? this.fileService.canMove(source, target, overwrite) : this.fileService.canCopy(source, target, overwrite));
|
||||
if (validateMoveOrCopy instanceof Error) {
|
||||
throw validateMoveOrCopy;
|
||||
}
|
||||
}
|
||||
|
||||
// file operation participant
|
||||
await this.runFileOperationParticipants(resource, undefined, FileOperation.DELETE);
|
||||
await this.runFileOperationParticipants(files, move ? FileOperation.MOVE : FileOperation.COPY);
|
||||
|
||||
// before events
|
||||
const event = { correlationId: this.correlationIds++, operation: FileOperation.DELETE, target: resource };
|
||||
// before event
|
||||
const event = { correlationId: this.correlationIds++, operation: move ? FileOperation.MOVE : FileOperation.COPY, files };
|
||||
await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
// Check for any existing dirty working copies for the resource
|
||||
try {
|
||||
for (const { source, target } of files) {
|
||||
|
||||
// if source and target are not equal, handle dirty working copies
|
||||
// depending on the operation:
|
||||
// - move: revert both source and target (if any)
|
||||
// - copy: revert target (if any)
|
||||
if (!this.uriIdentityService.extUri.isEqual(source, target)) {
|
||||
const dirtyWorkingCopies = (move ? [...this.getDirty(source), ...this.getDirty(target)] : this.getDirty(target));
|
||||
await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true })));
|
||||
}
|
||||
|
||||
// now we can rename the source to target via file operation
|
||||
if (move) {
|
||||
stats.push(await this.fileService.move(source, target, overwrite));
|
||||
} else {
|
||||
stats.push(await this.fileService.copy(source, target, overwrite));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
// error event
|
||||
await this._onDidFailWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
// after event
|
||||
await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
async delete(resources: URI[], options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {
|
||||
|
||||
// validate delete operation before starting
|
||||
for (const resource of resources) {
|
||||
const validateDelete = await this.fileService.canDelete(resource, options);
|
||||
if (validateDelete instanceof Error) {
|
||||
throw validateDelete;
|
||||
}
|
||||
}
|
||||
|
||||
// file operation participant
|
||||
const files = resources.map(target => ({ target }));
|
||||
await this.runFileOperationParticipants(files, FileOperation.DELETE);
|
||||
|
||||
// before events
|
||||
const event = { correlationId: this.correlationIds++, operation: FileOperation.DELETE, files };
|
||||
await this._onWillRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
|
||||
// check for any existing dirty working copies for the resource
|
||||
// and do a soft revert before deleting to be able to close
|
||||
// any opened editor with these working copies
|
||||
const dirtyWorkingCopies = this.getDirty(resource);
|
||||
await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true })));
|
||||
for (const resource of resources) {
|
||||
const dirtyWorkingCopies = this.getDirty(resource);
|
||||
await Promise.all(dirtyWorkingCopies.map(dirtyWorkingCopy => dirtyWorkingCopy.revert({ soft: true })));
|
||||
}
|
||||
|
||||
// Now actually delete from disk
|
||||
// now actually delete from disk
|
||||
try {
|
||||
await this.fileService.del(resource, options);
|
||||
for (const resource of resources) {
|
||||
await this.fileService.del(resource, options);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
// error event
|
||||
@@ -308,6 +362,8 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
|
||||
await this._onDidRunWorkingCopyFileOperation.fireAsync(event, CancellationToken.None);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region File operation participants
|
||||
|
||||
@@ -317,8 +373,8 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
|
||||
return this.fileOperationParticipants.addFileOperationParticipant(participant);
|
||||
}
|
||||
|
||||
runFileOperationParticipants(target: URI, source: URI | undefined, operation: FileOperation): Promise<void> {
|
||||
return this.fileOperationParticipants.participate(target, source, operation);
|
||||
private runFileOperationParticipants(files: SourceTargetPair[], operation: FileOperation): Promise<void> {
|
||||
return this.fileOperationParticipants.participate(files, operation);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -12,11 +12,11 @@ import { workbenchInstantiationService, TestServiceAccessor, TestTextFileEditorM
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { FileOperation } from 'vs/platform/files/common/files';
|
||||
import { TestWorkingCopy } from 'vs/workbench/services/workingCopy/test/common/workingCopyService.test';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
suite('WorkingCopyFileService', () => {
|
||||
|
||||
let instantiationService: IInstantiationService;
|
||||
let model: TextFileEditorModel;
|
||||
let accessor: TestServiceAccessor;
|
||||
|
||||
setup(() => {
|
||||
@@ -25,144 +25,120 @@ suite('WorkingCopyFileService', () => {
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
model?.dispose();
|
||||
(<TextFileEditorModelManager>accessor.textFileService.files).dispose();
|
||||
});
|
||||
|
||||
test('create - dirty file', async function () {
|
||||
await testCreate(toResource.call(this, '/path/file.txt'), VSBuffer.fromString('Hello World'));
|
||||
});
|
||||
|
||||
test('delete - dirty file', async function () {
|
||||
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model.resource, model);
|
||||
await testDelete([toResource.call(this, '/path/file.txt')]);
|
||||
});
|
||||
|
||||
await model.load();
|
||||
model!.textEditorModel!.setValue('foo');
|
||||
assert.ok(accessor.workingCopyService.isDirty(model.resource));
|
||||
|
||||
let eventCounter = 0;
|
||||
let correlationId: number | undefined = undefined;
|
||||
|
||||
const participant = accessor.workingCopyFileService.addFileOperationParticipant({
|
||||
participate: async (target, source, operation) => {
|
||||
assert.equal(target.toString(), model.resource.toString());
|
||||
assert.equal(operation, FileOperation.DELETE);
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
|
||||
assert.equal(e.target.toString(), model.resource.toString());
|
||||
assert.equal(e.operation, FileOperation.DELETE);
|
||||
correlationId = e.correlationId;
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
assert.equal(e.target.toString(), model.resource.toString());
|
||||
assert.equal(e.operation, FileOperation.DELETE);
|
||||
assert.equal(e.correlationId, correlationId);
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
await accessor.workingCopyFileService.delete(model.resource);
|
||||
assert.ok(!accessor.workingCopyService.isDirty(model.resource));
|
||||
|
||||
assert.equal(eventCounter, 3);
|
||||
|
||||
participant.dispose();
|
||||
listener1.dispose();
|
||||
listener2.dispose();
|
||||
test('delete multiple - dirty files', async function () {
|
||||
await testDelete([
|
||||
toResource.call(this, '/path/file1.txt'),
|
||||
toResource.call(this, '/path/file2.txt'),
|
||||
toResource.call(this, '/path/file3.txt'),
|
||||
toResource.call(this, '/path/file4.txt')]);
|
||||
});
|
||||
|
||||
test('move - dirty file', async function () {
|
||||
await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), true);
|
||||
await testMoveOrCopy([{ source: toResource.call(this, '/path/file.txt'), target: toResource.call(this, '/path/file_target.txt') }], true);
|
||||
});
|
||||
|
||||
test('move - source identical to target', async function () {
|
||||
let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel.resource, sourceModel);
|
||||
|
||||
const eventCounter = await testEventsMoveOrCopy([{ source: sourceModel.resource, target: sourceModel.resource }], true);
|
||||
|
||||
sourceModel.dispose();
|
||||
assert.equal(eventCounter, 3);
|
||||
});
|
||||
|
||||
test('move - one source == target and another source != target', async function () {
|
||||
let sourceModel1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file1.txt'), 'utf8', undefined);
|
||||
let sourceModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file2.txt'), 'utf8', undefined);
|
||||
let targetModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target2.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel1.resource, sourceModel1);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel2.resource, sourceModel2);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(targetModel2.resource, targetModel2);
|
||||
|
||||
const eventCounter = await testEventsMoveOrCopy([
|
||||
{ source: sourceModel1.resource, target: sourceModel1.resource },
|
||||
{ source: sourceModel2.resource, target: targetModel2.resource }
|
||||
], true);
|
||||
|
||||
sourceModel1.dispose();
|
||||
sourceModel2.dispose();
|
||||
targetModel2.dispose();
|
||||
assert.equal(eventCounter, 3);
|
||||
});
|
||||
|
||||
test('move multiple - dirty file', async function () {
|
||||
await testMoveOrCopy([
|
||||
{ source: toResource.call(this, '/path/file1.txt'), target: toResource.call(this, '/path/file1_target.txt') },
|
||||
{ source: toResource.call(this, '/path/file2.txt'), target: toResource.call(this, '/path/file2_target.txt') }],
|
||||
true);
|
||||
});
|
||||
|
||||
test('move - dirty file (target exists and is dirty)', async function () {
|
||||
await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), true, true);
|
||||
await testMoveOrCopy([{ source: toResource.call(this, '/path/file.txt'), target: toResource.call(this, '/path/file_target.txt') }], true, true);
|
||||
});
|
||||
|
||||
test('copy - dirty file', async function () {
|
||||
await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), false);
|
||||
await testMoveOrCopy([{ source: toResource.call(this, '/path/file.txt'), target: toResource.call(this, '/path/file_target.txt') }], false);
|
||||
});
|
||||
|
||||
test('copy - source identical to target', async function () {
|
||||
let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel.resource, sourceModel);
|
||||
|
||||
const eventCounter = await testEventsMoveOrCopy([{ source: sourceModel.resource, target: sourceModel.resource }]);
|
||||
|
||||
sourceModel.dispose();
|
||||
assert.equal(eventCounter, 3);
|
||||
});
|
||||
|
||||
test('copy - one source == target and another source != target', async function () {
|
||||
let sourceModel1: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file1.txt'), 'utf8', undefined);
|
||||
let sourceModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file2.txt'), 'utf8', undefined);
|
||||
let targetModel2: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target2.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel1.resource, sourceModel1);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel2.resource, sourceModel2);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(targetModel2.resource, targetModel2);
|
||||
|
||||
const eventCounter = await testEventsMoveOrCopy([
|
||||
{ source: sourceModel1.resource, target: sourceModel1.resource },
|
||||
{ source: sourceModel2.resource, target: targetModel2.resource }
|
||||
]);
|
||||
|
||||
sourceModel1.dispose();
|
||||
sourceModel2.dispose();
|
||||
targetModel2.dispose();
|
||||
assert.equal(eventCounter, 3);
|
||||
});
|
||||
|
||||
test('copy multiple - dirty file', async function () {
|
||||
await testMoveOrCopy([
|
||||
{ source: toResource.call(this, '/path/file1.txt'), target: toResource.call(this, '/path/file_target1.txt') },
|
||||
{ source: toResource.call(this, '/path/file2.txt'), target: toResource.call(this, '/path/file_target2.txt') },
|
||||
{ source: toResource.call(this, '/path/file3.txt'), target: toResource.call(this, '/path/file_target3.txt') }],
|
||||
false);
|
||||
});
|
||||
|
||||
test('copy - dirty file (target exists and is dirty)', async function () {
|
||||
await testMoveOrCopy(toResource.call(this, '/path/file.txt'), toResource.call(this, '/path/file_target.txt'), false, true);
|
||||
await testMoveOrCopy([{ source: toResource.call(this, '/path/file.txt'), target: toResource.call(this, '/path/file_target.txt') }], false, true);
|
||||
});
|
||||
|
||||
async function testMoveOrCopy(source: URI, target: URI, move: boolean, targetDirty?: boolean): Promise<void> {
|
||||
let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, source, 'utf8', undefined);
|
||||
let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, target, 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel.resource, sourceModel);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(targetModel.resource, targetModel);
|
||||
|
||||
await sourceModel.load();
|
||||
sourceModel.textEditorModel!.setValue('foo');
|
||||
assert.ok(accessor.textFileService.isDirty(sourceModel.resource));
|
||||
|
||||
if (targetDirty) {
|
||||
await targetModel.load();
|
||||
targetModel.textEditorModel!.setValue('bar');
|
||||
assert.ok(accessor.textFileService.isDirty(targetModel.resource));
|
||||
}
|
||||
|
||||
let eventCounter = 0;
|
||||
let correlationId: number | undefined = undefined;
|
||||
|
||||
const participant = accessor.workingCopyFileService.addFileOperationParticipant({
|
||||
participate: async (target, source, operation) => {
|
||||
assert.equal(target.toString(), targetModel.resource.toString());
|
||||
assert.equal(source?.toString(), sourceModel.resource.toString());
|
||||
assert.equal(operation, move ? FileOperation.MOVE : FileOperation.COPY);
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
|
||||
assert.equal(e.target.toString(), targetModel.resource.toString());
|
||||
assert.equal(e.source?.toString(), sourceModel.resource.toString());
|
||||
assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY);
|
||||
eventCounter++;
|
||||
correlationId = e.correlationId;
|
||||
});
|
||||
|
||||
const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
assert.equal(e.target.toString(), targetModel.resource.toString());
|
||||
assert.equal(e.source?.toString(), sourceModel.resource.toString());
|
||||
assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY);
|
||||
eventCounter++;
|
||||
assert.equal(e.correlationId, correlationId);
|
||||
});
|
||||
|
||||
if (move) {
|
||||
await accessor.workingCopyFileService.move(sourceModel.resource, targetModel.resource, true);
|
||||
} else {
|
||||
await accessor.workingCopyFileService.copy(sourceModel.resource, targetModel.resource, true);
|
||||
}
|
||||
|
||||
assert.equal(targetModel.textEditorModel!.getValue(), 'foo');
|
||||
|
||||
if (move) {
|
||||
assert.ok(!accessor.textFileService.isDirty(sourceModel.resource));
|
||||
} else {
|
||||
assert.ok(accessor.textFileService.isDirty(sourceModel.resource));
|
||||
}
|
||||
assert.ok(accessor.textFileService.isDirty(targetModel.resource));
|
||||
|
||||
assert.equal(eventCounter, 3);
|
||||
|
||||
sourceModel.dispose();
|
||||
targetModel.dispose();
|
||||
|
||||
participant.dispose();
|
||||
listener1.dispose();
|
||||
listener2.dispose();
|
||||
}
|
||||
|
||||
test('getDirty', async function () {
|
||||
const model1 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-1.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model.resource, model);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model1.resource, model1);
|
||||
|
||||
const model2 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-2.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model.resource, model);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model2.resource, model2);
|
||||
|
||||
let dirty = accessor.workingCopyFileService.getDirty(model1.resource);
|
||||
assert.equal(dirty.length, 0);
|
||||
@@ -190,7 +166,7 @@ suite('WorkingCopyFileService', () => {
|
||||
|
||||
test('registerWorkingCopyProvider', async function () {
|
||||
const model1 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-1.txt'), 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model.resource, model);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model1.resource, model1);
|
||||
await model1.load();
|
||||
model1.textEditorModel!.setValue('foo');
|
||||
|
||||
@@ -212,4 +188,241 @@ suite('WorkingCopyFileService', () => {
|
||||
|
||||
model1.dispose();
|
||||
});
|
||||
|
||||
async function testEventsMoveOrCopy(files: { source: URI, target: URI }[], move?: boolean): Promise<number> {
|
||||
let eventCounter = 0;
|
||||
|
||||
const participant = accessor.workingCopyFileService.addFileOperationParticipant({
|
||||
participate: async files => {
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
if (move) {
|
||||
await accessor.workingCopyFileService.move(files, { overwrite: true });
|
||||
} else {
|
||||
await accessor.workingCopyFileService.copy(files, { overwrite: true });
|
||||
}
|
||||
|
||||
participant.dispose();
|
||||
listener1.dispose();
|
||||
listener2.dispose();
|
||||
return eventCounter;
|
||||
}
|
||||
|
||||
async function testMoveOrCopy(files: { source: URI, target: URI }[], move: boolean, targetDirty?: boolean): Promise<void> {
|
||||
|
||||
let eventCounter = 0;
|
||||
const models = await Promise.all(files.map(async ({ source, target }, i) => {
|
||||
let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, source, 'utf8', undefined);
|
||||
let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, target, 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(sourceModel.resource, sourceModel);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(targetModel.resource, targetModel);
|
||||
|
||||
await sourceModel.load();
|
||||
sourceModel.textEditorModel!.setValue('foo' + i);
|
||||
assert.ok(accessor.textFileService.isDirty(sourceModel.resource));
|
||||
if (targetDirty) {
|
||||
await targetModel.load();
|
||||
targetModel.textEditorModel!.setValue('bar' + i);
|
||||
assert.ok(accessor.textFileService.isDirty(targetModel.resource));
|
||||
}
|
||||
|
||||
return { sourceModel, targetModel };
|
||||
}));
|
||||
|
||||
const participant = accessor.workingCopyFileService.addFileOperationParticipant({
|
||||
participate: async (files, operation) => {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const { target, source } = files[i];
|
||||
const { targetModel, sourceModel } = models[i];
|
||||
|
||||
assert.equal(target.toString(), targetModel.resource.toString());
|
||||
assert.equal(source?.toString(), sourceModel.resource.toString());
|
||||
}
|
||||
|
||||
eventCounter++;
|
||||
|
||||
assert.equal(operation, move ? FileOperation.MOVE : FileOperation.COPY);
|
||||
}
|
||||
});
|
||||
|
||||
let correlationId: number;
|
||||
|
||||
const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
|
||||
for (let i = 0; i < e.files.length; i++) {
|
||||
const { target, source } = files[i];
|
||||
const { targetModel, sourceModel } = models[i];
|
||||
|
||||
assert.equal(target.toString(), targetModel.resource.toString());
|
||||
assert.equal(source?.toString(), sourceModel.resource.toString());
|
||||
}
|
||||
|
||||
eventCounter++;
|
||||
|
||||
correlationId = e.correlationId;
|
||||
assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY);
|
||||
});
|
||||
|
||||
const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
for (let i = 0; i < e.files.length; i++) {
|
||||
const { target, source } = files[i];
|
||||
const { targetModel, sourceModel } = models[i];
|
||||
assert.equal(target.toString(), targetModel.resource.toString());
|
||||
assert.equal(source?.toString(), sourceModel.resource.toString());
|
||||
}
|
||||
|
||||
eventCounter++;
|
||||
|
||||
assert.equal(e.operation, move ? FileOperation.MOVE : FileOperation.COPY);
|
||||
assert.equal(e.correlationId, correlationId);
|
||||
});
|
||||
|
||||
if (move) {
|
||||
await accessor.workingCopyFileService.move(models.map(model => ({ source: model.sourceModel.resource, target: model.targetModel.resource })), { overwrite: true });
|
||||
} else {
|
||||
await accessor.workingCopyFileService.copy(models.map(model => ({ source: model.sourceModel.resource, target: model.targetModel.resource })), { overwrite: true });
|
||||
}
|
||||
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
const { sourceModel, targetModel } = models[i];
|
||||
|
||||
assert.equal(targetModel.textEditorModel!.getValue(), 'foo' + i);
|
||||
|
||||
if (move) {
|
||||
assert.ok(!accessor.textFileService.isDirty(sourceModel.resource));
|
||||
} else {
|
||||
assert.ok(accessor.textFileService.isDirty(sourceModel.resource));
|
||||
}
|
||||
assert.ok(accessor.textFileService.isDirty(targetModel.resource));
|
||||
|
||||
sourceModel.dispose();
|
||||
targetModel.dispose();
|
||||
}
|
||||
assert.equal(eventCounter, 3);
|
||||
|
||||
participant.dispose();
|
||||
listener1.dispose();
|
||||
listener2.dispose();
|
||||
}
|
||||
|
||||
async function testDelete(resources: URI[]) {
|
||||
|
||||
const models = await Promise.all(resources.map(async resource => {
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, resource, 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model.resource, model);
|
||||
|
||||
await model.load();
|
||||
model!.textEditorModel!.setValue('foo');
|
||||
assert.ok(accessor.workingCopyService.isDirty(model.resource));
|
||||
return model;
|
||||
}));
|
||||
|
||||
let eventCounter = 0;
|
||||
let correlationId: number | undefined = undefined;
|
||||
|
||||
const participant = accessor.workingCopyFileService.addFileOperationParticipant({
|
||||
participate: async (files, operation) => {
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
const model = models[i];
|
||||
const file = files[i];
|
||||
assert.equal(file.target.toString(), model.resource.toString());
|
||||
}
|
||||
assert.equal(operation, FileOperation.DELETE);
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
const model = models[i];
|
||||
const file = e.files[i];
|
||||
assert.equal(file.target.toString(), model.resource.toString());
|
||||
}
|
||||
assert.equal(e.operation, FileOperation.DELETE);
|
||||
correlationId = e.correlationId;
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
const model = models[i];
|
||||
const file = e.files[i];
|
||||
assert.equal(file.target.toString(), model.resource.toString());
|
||||
}
|
||||
assert.equal(e.operation, FileOperation.DELETE);
|
||||
assert.equal(e.correlationId, correlationId);
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
await accessor.workingCopyFileService.delete(models.map(m => m.resource));
|
||||
for (const model of models) {
|
||||
assert.ok(!accessor.workingCopyService.isDirty(model.resource));
|
||||
model.dispose();
|
||||
}
|
||||
|
||||
assert.equal(eventCounter, 3);
|
||||
|
||||
participant.dispose();
|
||||
listener1.dispose();
|
||||
listener2.dispose();
|
||||
}
|
||||
|
||||
async function testCreate(resource: URI, contents: VSBuffer) {
|
||||
const model = instantiationService.createInstance(TextFileEditorModel, resource, 'utf8', undefined);
|
||||
(<TestTextFileEditorModelManager>accessor.textFileService.files).add(model.resource, model);
|
||||
|
||||
await model.load();
|
||||
model!.textEditorModel!.setValue('foo');
|
||||
assert.ok(accessor.workingCopyService.isDirty(model.resource));
|
||||
|
||||
let eventCounter = 0;
|
||||
let correlationId: number | undefined = undefined;
|
||||
|
||||
const participant = accessor.workingCopyFileService.addFileOperationParticipant({
|
||||
participate: async (files, operation) => {
|
||||
assert.equal(files.length, 1);
|
||||
const file = files[0];
|
||||
assert.equal(file.target.toString(), model.resource.toString());
|
||||
assert.equal(operation, FileOperation.CREATE);
|
||||
eventCounter++;
|
||||
}
|
||||
});
|
||||
|
||||
const listener1 = accessor.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => {
|
||||
assert.equal(e.files.length, 1);
|
||||
const file = e.files[0];
|
||||
assert.equal(file.target.toString(), model.resource.toString());
|
||||
assert.equal(e.operation, FileOperation.CREATE);
|
||||
correlationId = e.correlationId;
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
const listener2 = accessor.workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
assert.equal(e.files.length, 1);
|
||||
const file = e.files[0];
|
||||
assert.equal(file.target.toString(), model.resource.toString());
|
||||
assert.equal(e.operation, FileOperation.CREATE);
|
||||
assert.equal(e.correlationId, correlationId);
|
||||
eventCounter++;
|
||||
});
|
||||
|
||||
await accessor.workingCopyFileService.create(resource, contents);
|
||||
assert.ok(!accessor.workingCopyService.isDirty(model.resource));
|
||||
model.dispose();
|
||||
|
||||
assert.equal(eventCounter, 3);
|
||||
|
||||
participant.dispose();
|
||||
listener1.dispose();
|
||||
listener2.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -200,8 +200,11 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi
|
||||
const remoteAuthority = this.environmentService.configuration.remoteAuthority;
|
||||
const untitledWorkspace = await this.workspacesService.createUntitledWorkspace(folders, remoteAuthority);
|
||||
if (path) {
|
||||
await this.saveWorkspaceAs(untitledWorkspace, path);
|
||||
await this.workspacesService.deleteUntitledWorkspace(untitledWorkspace); // https://github.com/microsoft/vscode/issues/100276
|
||||
try {
|
||||
await this.saveWorkspaceAs(untitledWorkspace, path);
|
||||
} finally {
|
||||
await this.workspacesService.deleteUntitledWorkspace(untitledWorkspace); // https://github.com/microsoft/vscode/issues/100276
|
||||
}
|
||||
} else {
|
||||
path = untitledWorkspace.configPath;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user