未验证 提交 1cc556a9 编写于 作者: S Sandeep Somavarapu 提交者: GitHub

Merge branch 'master' into movableViews

......@@ -4,18 +4,18 @@
*--------------------------------------------------------------------------------------------*/
import * as crypto from 'crypto';
import * as vscode from 'vscode';
import * as https from 'https';
import * as querystring from 'querystring';
import { keychain } from './keychain';
import { toBase64UrlEncoding } from './utils';
import * as vscode from 'vscode';
import { createServer, startServer } from './authServer';
import { keychain } from './keychain';
import Logger from './logger';
import { toBase64UrlEncoding } from './utils';
const redirectUrl = 'https://vscode-redirect.azurewebsites.net/';
const loginEndpointUrl = 'https://login.microsoftonline.com/';
const clientId = 'aebc6443-996d-45c2-90f0-388ff96faa56';
const scope = 'https://management.core.windows.net/.default offline_access';
const resourceId = 'https://management.core.windows.net/';
const tenant = 'common';
interface IToken {
......@@ -24,6 +24,13 @@ interface IToken {
refreshToken: string;
}
interface ITokenClaims {
email?: string;
unique_name?: string;
oid?: string;
altsecid?: string;
}
export const onDidChangeSessions = new vscode.EventEmitter<void>();
export class AzureActiveDirectoryService {
......@@ -59,23 +66,20 @@ export class AzureActiveDirectoryService {
}
private tokenToAccount(token: IToken): vscode.Session {
const claims = this.getTokenClaims(token.accessToken);
return {
id: '',
id: claims?.oid || claims?.altsecid || '',
accessToken: token.accessToken,
displayName: this.getDisplayNameFromToken(token.accessToken)
displayName: claims?.email || claims?.unique_name || 'user@example.com'
};
}
private getDisplayNameFromToken(accessToken: string): string {
let displayName = 'user@example.com';
private getTokenClaims(accessToken: string): ITokenClaims | undefined {
try {
// TODO fixme
displayName = JSON.parse(atob(accessToken.split('.')[1]));
return JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64').toString());
} catch (e) {
// Fall back to example display name
Logger.error(e.message);
}
return displayName;
}
get sessions(): vscode.Session[] {
......@@ -108,7 +112,7 @@ export class AzureActiveDirectoryService {
const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64'));
const loginUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&scope=${encodeURIComponent(scope)}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`;
const loginUrl = `${loginEndpointUrl}${tenant}/oauth2/authorize?response_type=code&response_mode=query&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&resource=${encodeURIComponent(resourceId)}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`;
await redirectReq.res.writeHead(302, { Location: loginUrl });
redirectReq.res.end();
......@@ -165,12 +169,12 @@ export class AzureActiveDirectoryService {
grant_type: 'authorization_code',
code: code,
client_id: clientId,
scope: scope,
resource: resourceId,
code_verifier: codeVerifier,
redirect_uri: redirectUrl
});
const tokenUrl = vscode.Uri.parse(`${loginEndpointUrl}${tenant}/oauth2/v2.0/token`);
const tokenUrl = vscode.Uri.parse(`${loginEndpointUrl}${tenant}/oauth2/token`);
const post = https.request({
host: tokenUrl.authority,
......@@ -220,12 +224,12 @@ export class AzureActiveDirectoryService {
refresh_token: refreshToken,
client_id: clientId,
grant_type: 'refresh_token',
scope: scope
resource: resourceId
});
const post = https.request({
host: 'login.microsoftonline.com',
path: `/${tenant}/oauth2/v2.0/token`,
path: `/${tenant}/oauth2/token`,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
......
......@@ -12,8 +12,8 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionManagementChannel, GlobalExtensionEnablementServiceClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
......@@ -185,6 +185,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat
services.set(IUserDataAuthTokenService, new SyncDescriptor(UserDataAuthTokenService));
services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService));
services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', activeWindowRouter)));
services.set(IGlobalExtensionEnablementService, new GlobalExtensionEnablementServiceClient(server.getChannel('globalExtensionEnablement', activeWindowRouter)));
services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService));
services.set(ISettingsSyncService, new SyncDescriptor(SettingsSynchroniser));
services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService));
......
......@@ -87,6 +87,8 @@ export interface ParsedArgs {
'nolazy'?: boolean;
'force-device-scale-factor'?: string;
'force-renderer-accessibility'?: boolean;
'ignore-certificate-error'?: boolean;
'allow-insecure-localhost'?: boolean;
}
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
......
......@@ -119,6 +119,8 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
'nolazy': { type: 'boolean' }, // node inspect
'force-device-scale-factor': { type: 'string' },
'force-renderer-accessibility': { type: 'boolean' },
'ignore-certificate-error': { type: 'boolean' },
'allow-insecure-localhost': { type: 'boolean' },
'_urls': { type: 'string[]' },
_: { type: 'string[]' } // main arguments
......
......@@ -13,7 +13,7 @@ import { parseArgs, ErrorReporter, OPTIONS } from 'vs/platform/environment/node/
function parseAndValidate(cmdLineArgs: string[], reportWarnings: boolean): ParsedArgs {
const errorReporter: ErrorReporter = {
onUnknownOption: (id) => {
console.warn(localize('unknownOption', "Option '{0}' is unknown. Ignoring.", id));
console.warn(localize('unknownOption', "Warning: '{0}' is not in the list of known options, but still passed to Electron/Chromium.", id));
},
onMultipleValues: (id, val) => {
console.warn(localize('multipleValues', "Option '{0}' is defined more than once. Using value '{1}.'", id, val));
......
......@@ -44,6 +44,10 @@ export class GlobalExtensionEnablementService extends Disposable implements IGlo
return this._getExtensions(DISABLED_EXTENSIONS_STORAGE_PATH);
}
async getDisabledExtensionsAsync(): Promise<IExtensionIdentifier[]> {
return this.getDisabledExtensions();
}
private _addToDisabledExtensions(identifier: IExtensionIdentifier): boolean {
let disabledExtensions = this.getDisabledExtensions();
if (disabledExtensions.every(e => !areSameExtensions(e, identifier))) {
......
......@@ -214,9 +214,13 @@ export const IGlobalExtensionEnablementService = createDecorator<IGlobalExtensio
export interface IGlobalExtensionEnablementService {
_serviceBrand: undefined;
readonly onDidChangeEnablement: Event<readonly IExtensionIdentifier[]>;
getDisabledExtensions(): IExtensionIdentifier[];
enableExtension(extension: IExtensionIdentifier): Promise<boolean>;
disableExtension(extension: IExtensionIdentifier): Promise<boolean>;
// Async method until storage service is available in shared process
getDisabledExtensionsAsync(): Promise<IExtensionIdentifier[]>;
}
export const ExtensionsLabel = localize('extensions', "Extensions");
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Event } from 'vs/base/common/event';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IURITransformer, DefaultURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc';
......@@ -130,3 +130,53 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer
return Promise.resolve(this.channel.call('getExtensionsReport'));
}
}
export class GlobalExtensionEnablementServiceChannel implements IServerChannel {
constructor(private readonly service: IGlobalExtensionEnablementService) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChangeEnablement': return this.service.onDidChangeEnablement;
}
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case 'getDisabledExtensionsAsync': return Promise.resolve(this.service.getDisabledExtensions());
case 'enableExtension': return this.service.enableExtension(args[0]);
case 'disableExtension': return this.service.disableExtension(args[0]);
}
throw new Error('Invalid call');
}
}
export class GlobalExtensionEnablementServiceClient implements IGlobalExtensionEnablementService {
_serviceBrand: undefined;
get onDidChangeEnablement(): Event<IExtensionIdentifier[]> { return this.channel.listen('onDidChangeEnablement'); }
constructor(private readonly channel: IChannel) {
}
getDisabledExtensionsAsync(): Promise<IExtensionIdentifier[]> {
return this.channel.call('getDisabledExtensionsAsync');
}
enableExtension(extension: IExtensionIdentifier): Promise<boolean> {
return this.channel.call('enableExtension', [extension]);
}
disableExtension(extension: IExtensionIdentifier): Promise<boolean> {
return this.channel.call('disableExtension', [extension]);
}
getDisabledExtensions(): IExtensionIdentifier[] {
throw new Error('not supported');
}
}
......@@ -100,14 +100,8 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
// Remotely updated extensions
for (const key of values(baseToRemote.updated)) {
// If updated in local
if (baseToLocal.updated.has(key)) {
// Is different from local to remote
if (localToRemote.updated.has(key)) {
// update it in local
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
}
// Update in local always
updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
}
// Locally added extensions
......
......@@ -9,7 +9,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IFileService } from 'vs/platform/files/common/files';
......@@ -53,6 +53,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
@IFileService fileService: IFileService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IConfigurationService private readonly configurationService: IConfigurationService,
......@@ -285,6 +286,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
try {
await this.extensionManagementService.installFromGallery(extension);
removeFromSkipped.push(extension.identifier);
if (e.enabled) {
await this.extensionEnablementService.enableExtension(extension.identifier);
} else {
await this.extensionEnablementService.disableExtension(extension.identifier);
}
} catch (error) {
addToSkipped.push(e);
this.logService.error(error);
......@@ -312,8 +318,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
private async getLocalExtensions(): Promise<ISyncExtension[]> {
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
const disabledExtensions = await this.extensionEnablementService.getDisabledExtensionsAsync();
return installedExtensions
.map(({ identifier }) => ({ identifier, enabled: true }));
.map(({ identifier }) => ({ identifier, enabled: !disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier)) }));
}
private async getLastSyncUserData(): Promise<ILastSyncUserData | null> {
......
......@@ -835,7 +835,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
// Determine options
const openEditorOptions: IEditorOpenOptions = {
index: options ? options.index : undefined,
pinned: !this.accessor.partOptions.enablePreview || editor.isDirty() || options?.pinned || typeof options?.index === 'number',
pinned: !this.accessor.partOptions.enablePreview || editor.isDirty() || (options?.pinned ?? typeof options?.index === 'number'), // unless specified, prefer to pin when opening with index
active: this._group.count === 0 || !options || !options.inactive
};
......@@ -1497,7 +1497,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
}
options.inactive = !isActiveEditor;
options.pinned = true;
options.pinned = options.pinned ?? true; // unless specified, prefer to pin upon replace
const editorToReplace = { editor, replacement, options };
if (isActiveEditor) {
......
......@@ -175,7 +175,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor {
// because we are triggering another openEditor() call
// and do not control the initial intent that resulted
// in us now opening as binary.
const preservingOptions: IEditorOptions = { activation: EditorActivation.PRESERVE };
const preservingOptions: IEditorOptions = { activation: EditorActivation.PRESERVE, pinned: this.group?.isPinned(input) };
if (options) {
options.overwrite(preservingOptions);
} else {
......
......@@ -133,8 +133,7 @@ import { URI } from 'vs/base/common/uri';
'workbench.editor.mouseBackForwardToNavigate': {
'type': 'boolean',
'description': nls.localize('mouseBackForwardToNavigate', "Navigate between open files using mouse buttons four and five if provided."),
'default': true,
'included': !isMacintosh
'default': true
},
'workbench.editor.restoreViewState': {
'type': 'boolean',
......
......@@ -70,20 +70,17 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont
private async handleDirtyBeforeShutdown(workingCopies: IWorkingCopy[], reason: ShutdownReason): Promise<boolean> {
// Trigger backup if configured
let didBackup: boolean | undefined = undefined;
let backupError: Error | undefined = undefined;
if (this.filesConfigurationService.isHotExitEnabled) {
try {
didBackup = await this.backupBeforeShutdown(workingCopies, reason);
if (await this.backupBeforeShutdown(workingCopies, reason)) {
return this.noVeto({ discardAllBackups: false }); // no veto (backup was successful)
}
} catch (error) {
backupError = error;
}
}
if (didBackup) {
return this.noVeto({ discardAllBackups: false }); // no veto (backup was successful)
}
// we ran a backup but received an error that we show to the user
if (backupError) {
this.showErrorDialog(localize('backupTrackerBackupFailed', "One or many editors that are dirty could not be saved to the backup location."), backupError);
......@@ -169,7 +166,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont
await this.doSaveAllBeforeShutdown(true /* includeUntitled */, SaveReason.EXPLICIT);
if (this.workingCopyService.hasDirty) {
return true; // veto if any save failed or was canceled
return true; // veto (save failed or was canceled)
}
return this.noVeto({ discardAllBackups: true }); // no veto (dirty saved)
......@@ -194,10 +191,11 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont
// First save through the editor service to benefit
// from some extras like switching to untitled dirty
// editors before saving.
await this.editorService.saveAll({ includeUntitled, ...saveOptions });
const result = await this.editorService.saveAll({ includeUntitled, ...saveOptions });
// If we still have dirty working copies, save those directly
if (this.workingCopyService.hasDirty) {
// unless the save was not successful (e.g. cancelled)
if (result) {
await Promise.all(this.workingCopyService.dirtyWorkingCopies.map(async workingCopy => {
if (!includeUntitled && (workingCopy.capabilities & WorkingCopyCapabilities.Untitled)) {
return; // skip untitled unless explicitly included
......@@ -214,10 +212,11 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont
const revertOptions = { soft: true };
// First revert through the editor service
await this.editorService.revertAll(revertOptions);
const result = await this.editorService.revertAll(revertOptions);
// If we still have dirty working copies, revert those directly
if (this.workingCopyService.hasDirty) {
// unless the revert operation was not successful (e.g. cancelled)
if (result) {
await Promise.all(this.workingCopyService.dirtyWorkingCopies.map(workingCopy => workingCopy.revert(revertOptions)));
}
}
......
......@@ -441,7 +441,7 @@ export class CustomEditorContribution implements IWorkbenchContribution {
if (modifiedOverride || originalOverride) {
return {
override: (async () => {
const input = new DiffEditorInput(editor.getName(), editor.getDescription(), originalOverride || editor.originalInput, modifiedOverride || editor.modifiedInput);
const input = new DiffEditorInput(editor.getName(), editor.getDescription(), originalOverride || editor.originalInput, modifiedOverride || editor.modifiedInput, true);
return this.editorService.openEditor(input, { ...options, ignoreOverrides: true }, group);
})(),
};
......
......@@ -56,10 +56,11 @@ import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/ex
import { assertType } from 'vs/base/common/types';
import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet';
import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor';
import { SearchEditorInput, SearchEditorInputFactory, SearchEditorContribution } from 'vs/workbench/contrib/search/browser/searchEditorCommands';
import { SearchEditorInput, SearchEditorInputFactory, SearchEditorContribution } from 'vs/workbench/contrib/search/browser/searchEditorInput';
import { SearchEditor } from 'vs/workbench/contrib/search/browser/searchEditor';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor';
import product from 'vs/platform/product/common/product';
registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true);
registerSingleton(ISearchHistoryService, SearchHistoryService, true);
......@@ -854,7 +855,7 @@ configurationRegistry.registerConfiguration({
},
'search.enableSearchEditorPreview': {
type: 'boolean',
default: false,
default: product.quality !== 'stable',
description: nls.localize('search.enableSearchEditorPreview', "Experimental: When enabled, allows opening workspace search results in an editor.")
},
'search.searchEditorPreview.doubleClickBehaviour': {
......
......@@ -29,10 +29,9 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet';
import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel';
import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree';
import { createEditorFromSearchResult, openNewSearchEditor, SearchEditorInput } from 'vs/workbench/contrib/search/browser/searchEditorCommands';
import { createEditorFromSearchResult, openNewSearchEditor } from 'vs/workbench/contrib/search/browser/searchEditorActions';
import type { SearchEditor } from 'vs/workbench/contrib/search/browser/searchEditor';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { SearchEditorInput } from 'vs/workbench/contrib/search/browser/searchEditorInput';
export function isSearchViewFocused(viewletService: IViewletService, panelService: IPanelService): boolean {
const searchView = getSearchView(viewletService, panelService);
......@@ -561,7 +560,11 @@ export class OpenSearchEditorAction extends Action {
@IConfigurationService private configurationService: IConfigurationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super(id, label);
super(id, label, 'codicon-new-file');
}
update() {
// pass
}
get enabled(): boolean {
......@@ -587,7 +590,6 @@ export class OpenResultsInEditorAction extends Action {
@IEditorService private editorService: IEditorService,
@IConfigurationService private configurationService: IConfigurationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ITextFileService private readonly textFileService: ITextFileService
) {
super(id, label, 'codicon-go-to-file');
}
......@@ -604,7 +606,7 @@ export class OpenResultsInEditorAction extends Action {
async run() {
const searchView = getSearchView(this.viewletService, this.panelService);
if (searchView && this.configurationService.getValue<ISearchConfigurationProperties>('search').enableSearchEditorPreview) {
await createEditorFromSearchResult(searchView.searchResult, searchView.searchIncludePattern.getValue(), searchView.searchExcludePattern.getValue(), this.labelService, this.editorService, this.textFileService, this.instantiationService);
await createEditorFromSearchResult(searchView.searchResult, searchView.searchIncludePattern.getValue(), searchView.searchExcludePattern.getValue(), this.labelService, this.editorService, this.instantiationService);
}
}
}
......
......@@ -23,10 +23,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { ILabelService } from 'vs/platform/label/common/label';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorInput, EditorOptions } from 'vs/workbench/common/editor';
import { EditorOptions } from 'vs/workbench/common/editor';
import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget';
import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget';
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
......@@ -34,19 +34,15 @@ import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/co
import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel';
import { IPatternInfo, ISearchConfigurationProperties, ITextQuery } from 'vs/workbench/services/search/common/search';
import { Delayer } from 'vs/base/common/async';
import { serializeSearchResultForEditor, SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/search/browser/searchEditorCommands';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { serializeSearchResultForEditor } from 'vs/workbench/contrib/search/browser/searchEditorSerialization';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { InSearchEditor, InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants';
import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress';
import type { SearchEditorInput, SearchConfiguration } from 'vs/workbench/contrib/search/browser/searchEditorInput';
import { searchEditorFindMatchBorder, searchEditorFindMatch } from 'vs/platform/theme/common/colorRegistry';
const RESULT_LINE_REGEX = /^(\s+)(\d+)(:| )(\s+)(.*)$/;
// Using \r\n on Windows inserts an extra newline between results.
const lineDelimiter = '\n';
export class SearchEditor extends BaseEditor {
static readonly ID: string = 'workbench.editor.searchEditor';
......@@ -78,7 +74,6 @@ export class SearchEditor extends BaseEditor {
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextViewService private readonly contextViewService: IContextViewService,
@ICommandService private readonly commandService: ICommandService,
@ITextFileService private readonly textFileService: ITextFileService,
@IContextKeyService readonly contextKeyService: IContextKeyService,
@IEditorProgressService readonly progressService: IEditorProgressService,
) {
......@@ -304,16 +299,17 @@ export class SearchEditor extends BaseEditor {
return;
}
(assertIsDefined(this._input) as SearchEditorInput).setConfig(config);
const labelFormatter = (uri: URI): string => this.labelService.getUriLabel(uri, { relative: true });
const results = serializeSearchResultForEditor(searchModel.searchResult, config.includes, config.excludes, config.contextLines, labelFormatter, true);
const textModel = assertIsDefined(this.searchResultEditor.getModel());
this.modelService.updateModel(textModel, results.text.join(lineDelimiter));
this.modelService.updateModel(textModel, results.text);
this.getInput()?.setDirty(this.getInput()?.resource.scheme !== 'search-editor');
this.hideHeader();
textModel.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } })));
(assertIsDefined(this._input) as SearchEditorInput).reloadModel();
searchModel.dispose();
}
......@@ -327,7 +323,8 @@ export class SearchEditor extends BaseEditor {
.length
?? 0;
this.searchResultEditor.setHiddenAreas([new Range(1, 1, headerLines + 1, 1)]);
// const length = this.searchResultEditor.getModel()?.getLineLength(headerLines);
this.searchResultEditor.setHiddenAreas([new Range(1, 1, headerLines, 1)]);
}
layout(dimension: DOM.Dimension) {
......@@ -339,6 +336,14 @@ export class SearchEditor extends BaseEditor {
this.queryEditorWidget.focus();
}
getSelected() {
const selection = this.searchResultEditor.getSelection();
if (selection) {
return this.searchResultEditor.getModel()?.getValueInRange(selection) ?? '';
}
return '';
}
private reLayout() {
if (this.dimension) {
this.queryEditorWidget.setWidth(this.dimension.width - 28 /* container margin */);
......@@ -352,38 +357,26 @@ export class SearchEditor extends BaseEditor {
return this._input as SearchEditorInput;
}
async setInput(newInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
async setInput(newInput: SearchEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
await super.setInput(newInput, options, token);
this.inSearchEditorContextKey.set(true);
if (!(newInput instanceof SearchEditorInput)) { return; }
const model = assertIsDefined(this.modelService.getModel(newInput.resource));
const { model } = await newInput.reloadModel();
this.searchResultEditor.setModel(model);
const backup = await newInput.resolveBackup();
if (backup) {
model.setValueFromTextBuffer(backup);
} else {
if (newInput.resource.scheme !== 'search-editor') {
if (model.getValue() === '') {
model.setValue((await this.textFileService.read(newInput.resource)).value);
}
}
}
this.hideHeader();
this.pauseSearching = true;
this.queryEditorWidget.setValue(newInput.config.query, true);
this.queryEditorWidget.searchInput.setCaseSensitive(newInput.config.caseSensitive);
this.queryEditorWidget.searchInput.setRegex(newInput.config.regexp);
this.queryEditorWidget.searchInput.setWholeWords(newInput.config.wholeWord);
this.queryEditorWidget.setContextLines(newInput.config.contextLines);
this.inputPatternExcludes.setValue(newInput.config.excludes);
this.inputPatternIncludes.setValue(newInput.config.includes);
this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(newInput.config.useIgnores);
this.toggleIncludesExcludes(newInput.config.showIncludesExcludes);
const { query } = await newInput.reloadModel();
this.queryEditorWidget.setValue(query.query, true);
this.queryEditorWidget.searchInput.setCaseSensitive(query.caseSensitive);
this.queryEditorWidget.searchInput.setRegex(query.regexp);
this.queryEditorWidget.searchInput.setWholeWords(query.wholeWord);
this.queryEditorWidget.setContextLines(query.contextLines);
this.inputPatternExcludes.setValue(query.excludes);
this.inputPatternIncludes.setValue(query.includes);
this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(query.useIgnores);
this.toggleIncludesExcludes(query.showIncludesExcludes);
this.focusInput();
this.pauseSearching = false;
......@@ -415,3 +408,12 @@ export class SearchEditor extends BaseEditor {
this.inSearchEditorContextKey.set(false);
}
}
registerThemingParticipant((theme, collector) => {
collector.addRule(`.monaco-editor .searchEditorFindMatch { background-color: ${theme.getColor(searchEditorFindMatch)}; }`);
const findMatchHighlightBorder = theme.getColor(searchEditorFindMatchBorder);
if (findMatchHighlightBorder) {
collector.addRule(`.monaco-editor .searchEditorFindMatch { border: 1px ${theme.type === 'hc' ? 'dotted' : 'solid'} ${findMatchHighlightBorder}; box-sizing: border-box; }`);
}
});
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { assertIsDefined } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/searchEditor';
import { isDiffEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { TrackedRangeStickiness } from 'vs/editor/common/model';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILabelService } from 'vs/platform/label/common/label';
import { SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { SearchEditor } from 'vs/workbench/contrib/search/browser/searchEditor';
import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/search/browser/searchEditorInput';
import { serializeSearchResultForEditor, serializeSearchConfiguration } from 'vs/workbench/contrib/search/browser/searchEditorSerialization';
export const openNewSearchEditor =
async (editorService: IEditorService, instantiationService: IInstantiationService) => {
const activeEditor = editorService.activeTextEditorWidget;
let activeModel: ICodeEditor | undefined;
let selected = '';
if (activeEditor) {
if (isDiffEditor(activeEditor)) {
if (activeEditor.getOriginalEditor().hasTextFocus()) {
activeModel = activeEditor.getOriginalEditor();
} else {
activeModel = activeEditor.getModifiedEditor();
}
} else {
activeModel = activeEditor as ICodeEditor;
}
const selection = activeModel?.getSelection();
selected = (selection && activeModel?.getModel()?.getValueInRange(selection)) ?? '';
} else {
if (editorService.activeEditor instanceof SearchEditorInput) {
const active = editorService.activeControl as SearchEditor;
selected = active.getSelected();
}
}
const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { text: serializeSearchConfiguration({ query: selected }) });
await editorService.openEditor(input, { pinned: true });
};
export const createEditorFromSearchResult =
async (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, labelService: ILabelService, editorService: IEditorService, instantiationService: IInstantiationService) => {
if (!searchResult.query) {
console.error('Expected searchResult.query to be defined. Got', searchResult);
return;
}
const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true });
const { text, matchRanges } = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter, true);
const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { text });
const editor = await editorService.openEditor(input, { pinned: true }) as SearchEditor;
const model = assertIsDefined(editor.getModel());
model.deltaDecorations([], matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } })));
};
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/searchEditor';
import { coalesce, flatten } from 'vs/base/common/arrays';
import { repeat } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { Range } from 'vs/editor/common/core/range';
import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import { ITextQuery } from 'vs/workbench/services/search/common/search';
import { localize } from 'vs/nls';
import type { ITextModel } from 'vs/editor/common/model';
import type { SearchConfiguration } from 'vs/workbench/contrib/search/browser/searchEditorInput';
// Using \r\n on Windows inserts an extra newline between results.
const lineDelimiter = '\n';
const translateRangeLines =
(n: number) =>
(range: Range) =>
new Range(range.startLineNumber + n, range.startColumn, range.endLineNumber + n, range.endColumn);
const matchToSearchResultFormat = (match: Match): { line: string, ranges: Range[], lineNumber: string }[] => {
const getLinePrefix = (i: number) => `${match.range().startLineNumber + i}`;
const fullMatchLines = match.fullPreviewLines();
const largestPrefixSize = fullMatchLines.reduce((largest, _, i) => Math.max(getLinePrefix(i).length, largest), 0);
const results: { line: string, ranges: Range[], lineNumber: string }[] = [];
fullMatchLines
.forEach((sourceLine, i) => {
const lineNumber = getLinePrefix(i);
const paddingStr = repeat(' ', largestPrefixSize - lineNumber.length);
const prefix = ` ${lineNumber}: ${paddingStr}`;
const prefixOffset = prefix.length;
const line = (prefix + sourceLine).replace(/\r?\n?$/, '');
const rangeOnThisLine = ({ start, end }: { start?: number; end?: number; }) => new Range(1, (start ?? 1) + prefixOffset, 1, (end ?? sourceLine.length + 1) + prefixOffset);
const matchRange = match.range();
const matchIsSingleLine = matchRange.startLineNumber === matchRange.endLineNumber;
let lineRange;
if (matchIsSingleLine) { lineRange = (rangeOnThisLine({ start: matchRange.startColumn, end: matchRange.endColumn })); }
else if (i === 0) { lineRange = (rangeOnThisLine({ start: matchRange.startColumn })); }
else if (i === fullMatchLines.length - 1) { lineRange = (rangeOnThisLine({ end: matchRange.endColumn })); }
else { lineRange = (rangeOnThisLine({})); }
results.push({ lineNumber: lineNumber, line, ranges: [lineRange] });
});
return results;
};
type SearchResultSerialization = { text: string[], matchRanges: Range[] };
function fileMatchToSearchResultFormat(fileMatch: FileMatch, labelFormatter: (x: URI) => string): SearchResultSerialization {
const serializedMatches = flatten(fileMatch.matches()
.sort(searchMatchComparer)
.map(match => matchToSearchResultFormat(match)));
const uriString = labelFormatter(fileMatch.resource);
let text: string[] = [`${uriString}:`];
let matchRanges: Range[] = [];
const targetLineNumberToOffset: Record<string, number> = {};
const context: { line: string, lineNumber: number }[] = [];
fileMatch.context.forEach((line, lineNumber) => context.push({ line, lineNumber }));
context.sort((a, b) => a.lineNumber - b.lineNumber);
let lastLine: number | undefined = undefined;
const seenLines = new Set<string>();
serializedMatches.forEach(match => {
if (!seenLines.has(match.line)) {
while (context.length && context[0].lineNumber < +match.lineNumber) {
const { line, lineNumber } = context.shift()!;
if (lastLine !== undefined && lineNumber !== lastLine + 1) {
text.push('');
}
text.push(` ${lineNumber} ${line}`);
lastLine = lineNumber;
}
targetLineNumberToOffset[match.lineNumber] = text.length;
seenLines.add(match.line);
text.push(match.line);
lastLine = +match.lineNumber;
}
matchRanges.push(...match.ranges.map(translateRangeLines(targetLineNumberToOffset[match.lineNumber])));
});
while (context.length) {
const { line, lineNumber } = context.shift()!;
text.push(` ${lineNumber} ${line}`);
}
return { text, matchRanges };
}
const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes: string, excludes: string, contextLines: number): string[] => {
if (!pattern) { return []; }
const removeNullFalseAndUndefined = <T>(a: (T | null | false | undefined)[]) => a.filter(a => a !== false && a !== null && a !== undefined) as T[];
const escapeNewlines = (str: string) => str.replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
return removeNullFalseAndUndefined([
`# Query: ${escapeNewlines(pattern.contentPattern.pattern)}`,
(pattern.contentPattern.isCaseSensitive || pattern.contentPattern.isWordMatch || pattern.contentPattern.isRegExp || pattern.userDisabledExcludesAndIgnoreFiles)
&& `# Flags: ${coalesce([
pattern.contentPattern.isCaseSensitive && 'CaseSensitive',
pattern.contentPattern.isWordMatch && 'WordMatch',
pattern.contentPattern.isRegExp && 'RegExp',
pattern.userDisabledExcludesAndIgnoreFiles && 'IgnoreExcludeSettings'
]).join(' ')}`,
includes ? `# Including: ${includes}` : undefined,
excludes ? `# Excluding: ${excludes}` : undefined,
contextLines ? `# ContextLines: ${contextLines}` : undefined
]);
};
export const serializeSearchConfiguration = (config: Partial<SearchConfiguration>): string => {
const removeNullFalseAndUndefined = <T>(a: (T | null | false | undefined)[]) => a.filter(a => a !== false && a !== null && a !== undefined) as T[];
const escapeNewlines = (str: string) => str.replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
return removeNullFalseAndUndefined([
`# Query: ${escapeNewlines(config.query ?? '')}`,
(config.caseSensitive || config.wholeWord || config.regexp || config.useIgnores === false)
&& `# Flags: ${coalesce([
config.caseSensitive && 'CaseSensitive',
config.wholeWord && 'WordMatch',
config.regexp && 'RegExp',
(config.useIgnores === false) && 'IgnoreExcludeSettings'
]).join(' ')}`,
config.includes ? `# Including: ${config.includes}` : undefined,
config.excludes ? `# Excluding: ${config.excludes}` : undefined,
config.contextLines ? `# ContextLines: ${config.contextLines}` : undefined,
''
]).join(lineDelimiter);
};
export const extractSearchQuery = (model: ITextModel): SearchConfiguration => {
const header = model.getValueInRange(new Range(1, 1, 6, 1)).split(lineDelimiter);
const query: SearchConfiguration = {
query: '',
includes: '',
excludes: '',
regexp: false,
caseSensitive: false,
useIgnores: true,
wholeWord: false,
contextLines: 0,
showIncludesExcludes: false,
};
const unescapeNewlines = (str: string) => {
let out = '';
for (let i = 0; i < str.length; i++) {
if (str[i] === '\\') {
i++;
const escaped = str[i];
if (escaped === 'n') {
out += '\n';
}
else if (escaped === '\\') {
out += '\\';
}
else {
throw Error(localize('invalidQueryStringError', "All backslashes in Query string must be escaped (\\\\)"));
}
} else {
out += str[i];
}
}
return out;
};
const parseYML = /^# ([^:]*): (.*)$/;
for (const line of header) {
const parsed = parseYML.exec(line);
if (!parsed) { continue; }
const [, key, value] = parsed;
switch (key) {
case 'Query': query.query = unescapeNewlines(value); break;
case 'Including': query.includes = value; break;
case 'Excluding': query.excludes = value; break;
case 'ContextLines': query.contextLines = +value; break;
case 'Flags': {
query.regexp = value.indexOf('RegExp') !== -1;
query.caseSensitive = value.indexOf('CaseSensitive') !== -1;
query.useIgnores = value.indexOf('IgnoreExcludeSettings') === -1;
query.wholeWord = value.indexOf('WordMatch') !== -1;
}
}
}
query.showIncludesExcludes = !!(query.includes || query.excludes || !query.useIgnores);
return query;
};
export const serializeSearchResultForEditor =
(searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string, includeHeader: boolean): { matchRanges: Range[], text: string } => {
const header = includeHeader
? contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines)
: [];
const allResults =
flattenSearchResultSerializations(
flatten(
searchResult.folderMatches().sort(searchMatchComparer)
.map(folderMatch => folderMatch.matches().sort(searchMatchComparer)
.map(fileMatch => fileMatchToSearchResultFormat(fileMatch, labelFormatter)))));
return {
matchRanges: allResults.matchRanges.map(translateRangeLines(header.length)),
text: header
.concat(allResults.text.length ? allResults.text : ['No Results'])
.join(lineDelimiter)
};
};
const flattenSearchResultSerializations = (serializations: SearchResultSerialization[]): SearchResultSerialization => {
let text: string[] = [];
let matchRanges: Range[] = [];
serializations.forEach(serialized => {
serialized.matchRanges.map(translateRangeLines(text.length)).forEach(range => matchRanges.push(range));
serialized.text.forEach(line => text.push(line));
text.push(''); // new line
});
return { text, matchRanges };
};
......@@ -43,7 +43,7 @@ import { OpenFileFolderAction, OpenFolderAction } from 'vs/workbench/browser/act
import { ResourceLabels } from 'vs/workbench/browser/labels';
import { IEditor } from 'vs/workbench/common/editor';
import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget';
import { CancelSearchAction, ClearSearchResultsAction, CollapseDeepestExpandedLevelAction, RefreshAction, IFindInFilesArgs, OpenResultsInEditorAction, appendKeyBindingLabel, ExpandAllAction, ToggleCollapseAndExpandAction } from 'vs/workbench/contrib/search/browser/searchActions';
import { CancelSearchAction, ClearSearchResultsAction, CollapseDeepestExpandedLevelAction, RefreshAction, IFindInFilesArgs, OpenSearchEditorAction, appendKeyBindingLabel, ExpandAllAction, ToggleCollapseAndExpandAction } from 'vs/workbench/contrib/search/browser/searchActions';
import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate, SearchDND } from 'vs/workbench/contrib/search/browser/searchResultsView';
import { ISearchWidgetOptions, SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget';
import * as Constants from 'vs/workbench/contrib/search/common/constants';
......@@ -64,7 +64,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { MultiCursorSelectionController } from 'vs/editor/contrib/multicursor/multicursor';
import { Selection } from 'vs/editor/common/core/selection';
import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme';
import { createEditorFromSearchResult } from 'vs/workbench/contrib/search/browser/searchEditorCommands';
import { createEditorFromSearchResult } from 'vs/workbench/contrib/search/browser/searchEditorActions';
import { ILabelService } from 'vs/platform/label/common/label';
import { Color, RGBA } from 'vs/base/common/color';
......@@ -114,7 +114,7 @@ export class SearchView extends ViewPane {
private state: SearchUIState = SearchUIState.Idle;
private actions: Array<CollapseDeepestExpandedLevelAction | ClearSearchResultsAction | OpenResultsInEditorAction> = [];
private actions: Array<CollapseDeepestExpandedLevelAction | ClearSearchResultsAction | OpenSearchEditorAction> = [];
private toggleCollapseAction: ToggleCollapseAndExpandAction;
private cancelAction: CancelSearchAction;
private refreshAction: RefreshAction;
......@@ -232,7 +232,7 @@ export class SearchView extends ViewPane {
if (this.searchConfig.enableSearchEditorPreview) {
this.actions.push(
this._register(this.instantiationService.createInstance(OpenResultsInEditorAction, OpenResultsInEditorAction.ID, OpenResultsInEditorAction.LABEL))
this._register(this.instantiationService.createInstance(OpenSearchEditorAction, OpenSearchEditorAction.ID, OpenSearchEditorAction.LABEL))
);
}
......@@ -1559,7 +1559,7 @@ export class SearchView extends ViewPane {
this.messageDisposables.push(dom.addDisposableListener(openInEditorLink, dom.EventType.CLICK, (e: MouseEvent) => {
dom.EventHelper.stop(e, false);
createEditorFromSearchResult(this.searchResult, this.searchIncludePattern.getValue(), this.searchExcludePattern.getValue(), this.labelService, this.editorService, this.textFileService, this.instantiationService);
createEditorFromSearchResult(this.searchResult, this.searchIncludePattern.getValue(), this.searchExcludePattern.getValue(), this.labelService, this.editorService, this.instantiationService);
}));
} else {
......
......@@ -9,14 +9,18 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { UserDataSycnUtilServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc';
import { GlobalExtensionEnablementServiceChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
import { IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
class UserDataSyncServicesContribution implements IWorkbenchContribution {
constructor(
@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
@ISharedProcessService sharedProcessService: ISharedProcessService,
@IGlobalExtensionEnablementService globalExtensionEnablementService: IGlobalExtensionEnablementService,
) {
sharedProcessService.registerChannel('userDataSyncUtil', new UserDataSycnUtilServiceChannel(userDataSyncUtilService));
sharedProcessService.registerChannel('globalExtensionEnablement', new GlobalExtensionEnablementServiceChannel(globalExtensionEnablementService));
}
}
......
......@@ -28,6 +28,10 @@ export interface IBackupFileService {
/**
* Finds out if the provided resource with the given version is backed up.
*
* Note: if the backup service has not been initialized yet, this may return
* the wrong result. Always use `resolve()` if you can do a long running
* operation.
*/
hasBackupSync(resource: URI, versionId?: number): boolean;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册