Merge from vscode 2cd495805cf99b31b6926f08ff4348124b2cf73d

This commit is contained in:
ADS Merger
2020-06-30 04:40:21 +00:00
committed by AzureDataStudio
parent a8a7559229
commit 1388493cc1
602 changed files with 16375 additions and 12940 deletions

View File

@@ -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)
}
});

View File

@@ -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;

View 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));
}
}

View 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);
}
}
}

View File

@@ -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);

View File

@@ -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();

View File

@@ -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);

View File

@@ -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, '.')] };
}

View File

@@ -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> {

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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
}

View File

@@ -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);
}

View 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);
}

View File

@@ -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[];
}

View File

@@ -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();

View File

@@ -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) }
];
}
}

View File

@@ -24,8 +24,6 @@ export interface INativeWorkbenchEnvironmentService extends IWorkbenchEnvironmen
readonly log?: string;
readonly extHostLogsPath: URI;
readonly userHome: URI;
}
export interface INativeEnvironmentConfiguration extends IEnvironmentConfiguration, INativeWindowConfiguration { }

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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>;
}

View File

@@ -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}`);
}
}

View File

@@ -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);
}
}

View File

@@ -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'); }
}

View File

@@ -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);

View File

@@ -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}`);
}
}

View File

@@ -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;
}
};
}

View File

@@ -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), []);
}

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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
});
}

View File

@@ -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;

View File

@@ -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'
},
{

View File

@@ -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');

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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"));

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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> {

View 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[];
}

View 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}; }`);
}
});

View 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 {
}
}

View 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;
}

View File

@@ -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();
});
}
}

View File

@@ -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);
}
}

View File

@@ -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>;
}

View File

@@ -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);
}
}

View File

@@ -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, '/');
}

View File

@@ -15,7 +15,7 @@ export class NativePathService extends AbstractPathService {
@IRemoteAgentService remoteAgentService: IRemoteAgentService,
@IWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService
) {
super(() => environmentService.userHome, remoteAgentService);
super(environmentService.userHome, remoteAgentService);
}
}

View File

@@ -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);
}

View File

@@ -29,7 +29,7 @@ export class PreferencesEditorInput extends SideBySideEditorInput {
}
getTitle(verbosity: Verbosity): string {
return this.master.getTitle(verbosity);
return this.primary.getTitle(verbosity);
}
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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.
*/

View File

@@ -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';

View File

@@ -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);

View File

@@ -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;
}
}

View 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 };
}

View File

@@ -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;
}
}

View File

@@ -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.

View File

@@ -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);

View File

@@ -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++;
});

View File

@@ -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'));

View File

@@ -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);
});
});
});

View File

@@ -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"

View File

@@ -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;
}

View File

@@ -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.

After

Width:  |  Height:  |  Size: 151 B

View File

@@ -0,0 +1 @@
VSCODEは最高のエディタだ。

View File

@@ -0,0 +1,3 @@
<?xml>
</xml>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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));
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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
) {

View File

@@ -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);

View File

@@ -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

View File

@@ -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();
}
});

View File

@@ -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;
}