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

Merge branch 'master' into movableViews

...@@ -4,18 +4,18 @@ ...@@ -4,18 +4,18 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import * as vscode from 'vscode';
import * as https from 'https'; import * as https from 'https';
import * as querystring from 'querystring'; import * as querystring from 'querystring';
import { keychain } from './keychain'; import * as vscode from 'vscode';
import { toBase64UrlEncoding } from './utils';
import { createServer, startServer } from './authServer'; import { createServer, startServer } from './authServer';
import { keychain } from './keychain';
import Logger from './logger'; import Logger from './logger';
import { toBase64UrlEncoding } from './utils';
const redirectUrl = 'https://vscode-redirect.azurewebsites.net/'; const redirectUrl = 'https://vscode-redirect.azurewebsites.net/';
const loginEndpointUrl = 'https://login.microsoftonline.com/'; const loginEndpointUrl = 'https://login.microsoftonline.com/';
const clientId = 'aebc6443-996d-45c2-90f0-388ff96faa56'; 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'; const tenant = 'common';
interface IToken { interface IToken {
...@@ -24,6 +24,13 @@ interface IToken { ...@@ -24,6 +24,13 @@ interface IToken {
refreshToken: string; refreshToken: string;
} }
interface ITokenClaims {
email?: string;
unique_name?: string;
oid?: string;
altsecid?: string;
}
export const onDidChangeSessions = new vscode.EventEmitter<void>(); export const onDidChangeSessions = new vscode.EventEmitter<void>();
export class AzureActiveDirectoryService { export class AzureActiveDirectoryService {
...@@ -59,23 +66,20 @@ export class AzureActiveDirectoryService { ...@@ -59,23 +66,20 @@ export class AzureActiveDirectoryService {
} }
private tokenToAccount(token: IToken): vscode.Session { private tokenToAccount(token: IToken): vscode.Session {
const claims = this.getTokenClaims(token.accessToken);
return { return {
id: '', id: claims?.oid || claims?.altsecid || '',
accessToken: token.accessToken, accessToken: token.accessToken,
displayName: this.getDisplayNameFromToken(token.accessToken) displayName: claims?.email || claims?.unique_name || 'user@example.com'
}; };
} }
private getDisplayNameFromToken(accessToken: string): string { private getTokenClaims(accessToken: string): ITokenClaims | undefined {
let displayName = 'user@example.com';
try { try {
// TODO fixme return JSON.parse(Buffer.from(accessToken.split('.')[1], 'base64').toString());
displayName = JSON.parse(atob(accessToken.split('.')[1]));
} catch (e) { } catch (e) {
// Fall back to example display name Logger.error(e.message);
} }
return displayName;
} }
get sessions(): vscode.Session[] { get sessions(): vscode.Session[] {
...@@ -108,7 +112,7 @@ export class AzureActiveDirectoryService { ...@@ -108,7 +112,7 @@ export class AzureActiveDirectoryService {
const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64')); const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64'));
const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('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 }); await redirectReq.res.writeHead(302, { Location: loginUrl });
redirectReq.res.end(); redirectReq.res.end();
...@@ -165,12 +169,12 @@ export class AzureActiveDirectoryService { ...@@ -165,12 +169,12 @@ export class AzureActiveDirectoryService {
grant_type: 'authorization_code', grant_type: 'authorization_code',
code: code, code: code,
client_id: clientId, client_id: clientId,
scope: scope, resource: resourceId,
code_verifier: codeVerifier, code_verifier: codeVerifier,
redirect_uri: redirectUrl 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({ const post = https.request({
host: tokenUrl.authority, host: tokenUrl.authority,
...@@ -220,12 +224,12 @@ export class AzureActiveDirectoryService { ...@@ -220,12 +224,12 @@ export class AzureActiveDirectoryService {
refresh_token: refreshToken, refresh_token: refreshToken,
client_id: clientId, client_id: clientId,
grant_type: 'refresh_token', grant_type: 'refresh_token',
scope: scope resource: resourceId
}); });
const post = https.request({ const post = https.request({
host: 'login.microsoftonline.com', host: 'login.microsoftonline.com',
path: `/${tenant}/oauth2/v2.0/token`, path: `/${tenant}/oauth2/token`,
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
......
...@@ -12,8 +12,8 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; ...@@ -12,8 +12,8 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { ExtensionManagementChannel, GlobalExtensionEnablementServiceClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc';
import { IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService';
import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
...@@ -185,6 +185,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat ...@@ -185,6 +185,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat
services.set(IUserDataAuthTokenService, new SyncDescriptor(UserDataAuthTokenService)); services.set(IUserDataAuthTokenService, new SyncDescriptor(UserDataAuthTokenService));
services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService));
services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', activeWindowRouter))); 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(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService));
services.set(ISettingsSyncService, new SyncDescriptor(SettingsSynchroniser)); services.set(ISettingsSyncService, new SyncDescriptor(SettingsSynchroniser));
services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService));
......
...@@ -87,6 +87,8 @@ export interface ParsedArgs { ...@@ -87,6 +87,8 @@ export interface ParsedArgs {
'nolazy'?: boolean; 'nolazy'?: boolean;
'force-device-scale-factor'?: string; 'force-device-scale-factor'?: string;
'force-renderer-accessibility'?: boolean; 'force-renderer-accessibility'?: boolean;
'ignore-certificate-error'?: boolean;
'allow-insecure-localhost'?: boolean;
} }
export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService'); export const IEnvironmentService = createDecorator<IEnvironmentService>('environmentService');
......
...@@ -119,6 +119,8 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = { ...@@ -119,6 +119,8 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
'nolazy': { type: 'boolean' }, // node inspect 'nolazy': { type: 'boolean' }, // node inspect
'force-device-scale-factor': { type: 'string' }, 'force-device-scale-factor': { type: 'string' },
'force-renderer-accessibility': { type: 'boolean' }, 'force-renderer-accessibility': { type: 'boolean' },
'ignore-certificate-error': { type: 'boolean' },
'allow-insecure-localhost': { type: 'boolean' },
'_urls': { type: 'string[]' }, '_urls': { type: 'string[]' },
_: { type: 'string[]' } // main arguments _: { type: 'string[]' } // main arguments
......
...@@ -13,7 +13,7 @@ import { parseArgs, ErrorReporter, OPTIONS } from 'vs/platform/environment/node/ ...@@ -13,7 +13,7 @@ import { parseArgs, ErrorReporter, OPTIONS } from 'vs/platform/environment/node/
function parseAndValidate(cmdLineArgs: string[], reportWarnings: boolean): ParsedArgs { function parseAndValidate(cmdLineArgs: string[], reportWarnings: boolean): ParsedArgs {
const errorReporter: ErrorReporter = { const errorReporter: ErrorReporter = {
onUnknownOption: (id) => { 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) => { onMultipleValues: (id, val) => {
console.warn(localize('multipleValues', "Option '{0}' is defined more than once. Using value '{1}.'", 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 ...@@ -44,6 +44,10 @@ export class GlobalExtensionEnablementService extends Disposable implements IGlo
return this._getExtensions(DISABLED_EXTENSIONS_STORAGE_PATH); return this._getExtensions(DISABLED_EXTENSIONS_STORAGE_PATH);
} }
async getDisabledExtensionsAsync(): Promise<IExtensionIdentifier[]> {
return this.getDisabledExtensions();
}
private _addToDisabledExtensions(identifier: IExtensionIdentifier): boolean { private _addToDisabledExtensions(identifier: IExtensionIdentifier): boolean {
let disabledExtensions = this.getDisabledExtensions(); let disabledExtensions = this.getDisabledExtensions();
if (disabledExtensions.every(e => !areSameExtensions(e, identifier))) { if (disabledExtensions.every(e => !areSameExtensions(e, identifier))) {
......
...@@ -214,9 +214,13 @@ export const IGlobalExtensionEnablementService = createDecorator<IGlobalExtensio ...@@ -214,9 +214,13 @@ export const IGlobalExtensionEnablementService = createDecorator<IGlobalExtensio
export interface IGlobalExtensionEnablementService { export interface IGlobalExtensionEnablementService {
_serviceBrand: undefined; _serviceBrand: undefined;
readonly onDidChangeEnablement: Event<readonly IExtensionIdentifier[]>; readonly onDidChangeEnablement: Event<readonly IExtensionIdentifier[]>;
getDisabledExtensions(): IExtensionIdentifier[]; getDisabledExtensions(): IExtensionIdentifier[];
enableExtension(extension: IExtensionIdentifier): Promise<boolean>; enableExtension(extension: IExtensionIdentifier): Promise<boolean>;
disableExtension(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"); export const ExtensionsLabel = localize('extensions', "Extensions");
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; 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 { Event } from 'vs/base/common/event';
import { URI, UriComponents } from 'vs/base/common/uri'; import { URI, UriComponents } from 'vs/base/common/uri';
import { IURITransformer, DefaultURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IURITransformer, DefaultURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc';
...@@ -130,3 +130,53 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer ...@@ -130,3 +130,53 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer
return Promise.resolve(this.channel.call('getExtensionsReport')); 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,15 +100,9 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync ...@@ -100,15 +100,9 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
// Remotely updated extensions // Remotely updated extensions
for (const key of values(baseToRemote.updated)) { for (const key of values(baseToRemote.updated)) {
// If updated in local // Update in local always
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)); updated.push(massageSyncExtension(remoteExtensionsMap.get(key)!, key));
} }
}
}
// Locally added extensions // Locally added extensions
for (const key of values(baseToLocal.added)) { for (const key of values(baseToLocal.added)) {
......
...@@ -9,7 +9,7 @@ import { Emitter, Event } from 'vs/base/common/event'; ...@@ -9,7 +9,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources'; 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 { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IFileService } from 'vs/platform/files/common/files'; import { IFileService } from 'vs/platform/files/common/files';
...@@ -53,6 +53,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse ...@@ -53,6 +53,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
@IFileService fileService: IFileService, @IFileService fileService: IFileService,
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService,
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@IConfigurationService private readonly configurationService: IConfigurationService, @IConfigurationService private readonly configurationService: IConfigurationService,
...@@ -285,6 +286,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse ...@@ -285,6 +286,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
try { try {
await this.extensionManagementService.installFromGallery(extension); await this.extensionManagementService.installFromGallery(extension);
removeFromSkipped.push(extension.identifier); removeFromSkipped.push(extension.identifier);
if (e.enabled) {
await this.extensionEnablementService.enableExtension(extension.identifier);
} else {
await this.extensionEnablementService.disableExtension(extension.identifier);
}
} catch (error) { } catch (error) {
addToSkipped.push(e); addToSkipped.push(e);
this.logService.error(error); this.logService.error(error);
...@@ -312,8 +318,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse ...@@ -312,8 +318,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
private async getLocalExtensions(): Promise<ISyncExtension[]> { private async getLocalExtensions(): Promise<ISyncExtension[]> {
const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User);
const disabledExtensions = await this.extensionEnablementService.getDisabledExtensionsAsync();
return installedExtensions return installedExtensions
.map(({ identifier }) => ({ identifier, enabled: true })); .map(({ identifier }) => ({ identifier, enabled: !disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier)) }));
} }
private async getLastSyncUserData(): Promise<ILastSyncUserData | null> { private async getLastSyncUserData(): Promise<ILastSyncUserData | null> {
......
...@@ -835,7 +835,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { ...@@ -835,7 +835,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
// Determine options // Determine options
const openEditorOptions: IEditorOpenOptions = { const openEditorOptions: IEditorOpenOptions = {
index: options ? options.index : undefined, 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 active: this._group.count === 0 || !options || !options.inactive
}; };
...@@ -1497,7 +1497,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { ...@@ -1497,7 +1497,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
} }
options.inactive = !isActiveEditor; options.inactive = !isActiveEditor;
options.pinned = true; options.pinned = options.pinned ?? true; // unless specified, prefer to pin upon replace
const editorToReplace = { editor, replacement, options }; const editorToReplace = { editor, replacement, options };
if (isActiveEditor) { if (isActiveEditor) {
......
...@@ -175,7 +175,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor { ...@@ -175,7 +175,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditor {
// because we are triggering another openEditor() call // because we are triggering another openEditor() call
// and do not control the initial intent that resulted // and do not control the initial intent that resulted
// in us now opening as binary. // 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) { if (options) {
options.overwrite(preservingOptions); options.overwrite(preservingOptions);
} else { } else {
......
...@@ -133,8 +133,7 @@ import { URI } from 'vs/base/common/uri'; ...@@ -133,8 +133,7 @@ import { URI } from 'vs/base/common/uri';
'workbench.editor.mouseBackForwardToNavigate': { 'workbench.editor.mouseBackForwardToNavigate': {
'type': 'boolean', 'type': 'boolean',
'description': nls.localize('mouseBackForwardToNavigate', "Navigate between open files using mouse buttons four and five if provided."), 'description': nls.localize('mouseBackForwardToNavigate', "Navigate between open files using mouse buttons four and five if provided."),
'default': true, 'default': true
'included': !isMacintosh
}, },
'workbench.editor.restoreViewState': { 'workbench.editor.restoreViewState': {
'type': 'boolean', 'type': 'boolean',
......
...@@ -70,20 +70,17 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont ...@@ -70,20 +70,17 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont
private async handleDirtyBeforeShutdown(workingCopies: IWorkingCopy[], reason: ShutdownReason): Promise<boolean> { private async handleDirtyBeforeShutdown(workingCopies: IWorkingCopy[], reason: ShutdownReason): Promise<boolean> {
// Trigger backup if configured // Trigger backup if configured
let didBackup: boolean | undefined = undefined;
let backupError: Error | undefined = undefined; let backupError: Error | undefined = undefined;
if (this.filesConfigurationService.isHotExitEnabled) { if (this.filesConfigurationService.isHotExitEnabled) {
try { 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) { } catch (error) {
backupError = 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 // we ran a backup but received an error that we show to the user
if (backupError) { if (backupError) {
this.showErrorDialog(localize('backupTrackerBackupFailed', "One or many editors that are dirty could not be saved to the backup location."), 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 ...@@ -169,7 +166,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont
await this.doSaveAllBeforeShutdown(true /* includeUntitled */, SaveReason.EXPLICIT); await this.doSaveAllBeforeShutdown(true /* includeUntitled */, SaveReason.EXPLICIT);
if (this.workingCopyService.hasDirty) { 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) return this.noVeto({ discardAllBackups: true }); // no veto (dirty saved)
...@@ -194,10 +191,11 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont ...@@ -194,10 +191,11 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont
// First save through the editor service to benefit // First save through the editor service to benefit
// from some extras like switching to untitled dirty // from some extras like switching to untitled dirty
// editors before saving. // 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 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 => { await Promise.all(this.workingCopyService.dirtyWorkingCopies.map(async workingCopy => {
if (!includeUntitled && (workingCopy.capabilities & WorkingCopyCapabilities.Untitled)) { if (!includeUntitled && (workingCopy.capabilities & WorkingCopyCapabilities.Untitled)) {
return; // skip untitled unless explicitly included return; // skip untitled unless explicitly included
...@@ -214,10 +212,11 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont ...@@ -214,10 +212,11 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont
const revertOptions = { soft: true }; const revertOptions = { soft: true };
// First revert through the editor service // 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 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))); await Promise.all(this.workingCopyService.dirtyWorkingCopies.map(workingCopy => workingCopy.revert(revertOptions)));
} }
} }
......
...@@ -441,7 +441,7 @@ export class CustomEditorContribution implements IWorkbenchContribution { ...@@ -441,7 +441,7 @@ export class CustomEditorContribution implements IWorkbenchContribution {
if (modifiedOverride || originalOverride) { if (modifiedOverride || originalOverride) {
return { return {
override: (async () => { 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); return this.editorService.openEditor(input, { ...options, ignoreOverrides: true }, group);
})(), })(),
}; };
......
...@@ -56,10 +56,11 @@ import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/ex ...@@ -56,10 +56,11 @@ import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/ex
import { assertType } from 'vs/base/common/types'; import { assertType } from 'vs/base/common/types';
import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet'; import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet';
import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; 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 { SearchEditor } from 'vs/workbench/contrib/search/browser/searchEditor';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor';
import product from 'vs/platform/product/common/product';
registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true); registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true);
registerSingleton(ISearchHistoryService, SearchHistoryService, true); registerSingleton(ISearchHistoryService, SearchHistoryService, true);
...@@ -854,7 +855,7 @@ configurationRegistry.registerConfiguration({ ...@@ -854,7 +855,7 @@ configurationRegistry.registerConfiguration({
}, },
'search.enableSearchEditorPreview': { 'search.enableSearchEditorPreview': {
type: 'boolean', type: 'boolean',
default: false, default: product.quality !== 'stable',
description: nls.localize('search.enableSearchEditorPreview', "Experimental: When enabled, allows opening workspace search results in an editor.") description: nls.localize('search.enableSearchEditorPreview', "Experimental: When enabled, allows opening workspace search results in an editor.")
}, },
'search.searchEditorPreview.doubleClickBehaviour': { 'search.searchEditorPreview.doubleClickBehaviour': {
......
...@@ -29,10 +29,9 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; ...@@ -29,10 +29,9 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet'; import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet';
import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel'; import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel';
import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree'; 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 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 { export function isSearchViewFocused(viewletService: IViewletService, panelService: IPanelService): boolean {
const searchView = getSearchView(viewletService, panelService); const searchView = getSearchView(viewletService, panelService);
...@@ -561,7 +560,11 @@ export class OpenSearchEditorAction extends Action { ...@@ -561,7 +560,11 @@ export class OpenSearchEditorAction extends Action {
@IConfigurationService private configurationService: IConfigurationService, @IConfigurationService private configurationService: IConfigurationService,
@IInstantiationService private readonly instantiationService: IInstantiationService, @IInstantiationService private readonly instantiationService: IInstantiationService,
) { ) {
super(id, label); super(id, label, 'codicon-new-file');
}
update() {
// pass
} }
get enabled(): boolean { get enabled(): boolean {
...@@ -587,7 +590,6 @@ export class OpenResultsInEditorAction extends Action { ...@@ -587,7 +590,6 @@ export class OpenResultsInEditorAction extends Action {
@IEditorService private editorService: IEditorService, @IEditorService private editorService: IEditorService,
@IConfigurationService private configurationService: IConfigurationService, @IConfigurationService private configurationService: IConfigurationService,
@IInstantiationService private readonly instantiationService: IInstantiationService, @IInstantiationService private readonly instantiationService: IInstantiationService,
@ITextFileService private readonly textFileService: ITextFileService
) { ) {
super(id, label, 'codicon-go-to-file'); super(id, label, 'codicon-go-to-file');
} }
...@@ -604,7 +606,7 @@ export class OpenResultsInEditorAction extends Action { ...@@ -604,7 +606,7 @@ export class OpenResultsInEditorAction extends Action {
async run() { async run() {
const searchView = getSearchView(this.viewletService, this.panelService); const searchView = getSearchView(this.viewletService, this.panelService);
if (searchView && this.configurationService.getValue<ISearchConfigurationProperties>('search').enableSearchEditorPreview) { 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 ...@@ -23,10 +23,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { ILabelService } from 'vs/platform/label/common/label'; import { ILabelService } from 'vs/platform/label/common/label';
import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; 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 { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; 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 { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget';
import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget';
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
...@@ -34,19 +34,15 @@ import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/co ...@@ -34,19 +34,15 @@ import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/co
import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel';
import { IPatternInfo, ISearchConfigurationProperties, ITextQuery } from 'vs/workbench/services/search/common/search'; import { IPatternInfo, ISearchConfigurationProperties, ITextQuery } from 'vs/workbench/services/search/common/search';
import { Delayer } from 'vs/base/common/async'; import { Delayer } from 'vs/base/common/async';
import { serializeSearchResultForEditor, SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/search/browser/searchEditorCommands'; import { serializeSearchResultForEditor } from 'vs/workbench/contrib/search/browser/searchEditorSerialization';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { InSearchEditor, InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants'; import { InSearchEditor, InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants';
import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress'; 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+)(.*)$/; 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 { export class SearchEditor extends BaseEditor {
static readonly ID: string = 'workbench.editor.searchEditor'; static readonly ID: string = 'workbench.editor.searchEditor';
...@@ -78,7 +74,6 @@ export class SearchEditor extends BaseEditor { ...@@ -78,7 +74,6 @@ export class SearchEditor extends BaseEditor {
@IInstantiationService private readonly instantiationService: IInstantiationService, @IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextViewService private readonly contextViewService: IContextViewService, @IContextViewService private readonly contextViewService: IContextViewService,
@ICommandService private readonly commandService: ICommandService, @ICommandService private readonly commandService: ICommandService,
@ITextFileService private readonly textFileService: ITextFileService,
@IContextKeyService readonly contextKeyService: IContextKeyService, @IContextKeyService readonly contextKeyService: IContextKeyService,
@IEditorProgressService readonly progressService: IEditorProgressService, @IEditorProgressService readonly progressService: IEditorProgressService,
) { ) {
...@@ -304,16 +299,17 @@ export class SearchEditor extends BaseEditor { ...@@ -304,16 +299,17 @@ export class SearchEditor extends BaseEditor {
return; return;
} }
(assertIsDefined(this._input) as SearchEditorInput).setConfig(config);
const labelFormatter = (uri: URI): string => this.labelService.getUriLabel(uri, { relative: true }); 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 results = serializeSearchResultForEditor(searchModel.searchResult, config.includes, config.excludes, config.contextLines, labelFormatter, true);
const textModel = assertIsDefined(this.searchResultEditor.getModel()); 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.getInput()?.setDirty(this.getInput()?.resource.scheme !== 'search-editor');
this.hideHeader(); this.hideHeader();
textModel.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }))); textModel.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } })));
(assertIsDefined(this._input) as SearchEditorInput).reloadModel();
searchModel.dispose(); searchModel.dispose();
} }
...@@ -327,7 +323,8 @@ export class SearchEditor extends BaseEditor { ...@@ -327,7 +323,8 @@ export class SearchEditor extends BaseEditor {
.length .length
?? 0; ?? 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) { layout(dimension: DOM.Dimension) {
...@@ -339,6 +336,14 @@ export class SearchEditor extends BaseEditor { ...@@ -339,6 +336,14 @@ export class SearchEditor extends BaseEditor {
this.queryEditorWidget.focus(); this.queryEditorWidget.focus();
} }
getSelected() {
const selection = this.searchResultEditor.getSelection();
if (selection) {
return this.searchResultEditor.getModel()?.getValueInRange(selection) ?? '';
}
return '';
}
private reLayout() { private reLayout() {
if (this.dimension) { if (this.dimension) {
this.queryEditorWidget.setWidth(this.dimension.width - 28 /* container margin */); this.queryEditorWidget.setWidth(this.dimension.width - 28 /* container margin */);
...@@ -352,38 +357,26 @@ export class SearchEditor extends BaseEditor { ...@@ -352,38 +357,26 @@ export class SearchEditor extends BaseEditor {
return this._input as SearchEditorInput; 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); await super.setInput(newInput, options, token);
this.inSearchEditorContextKey.set(true); this.inSearchEditorContextKey.set(true);
if (!(newInput instanceof SearchEditorInput)) { return; } const { model } = await newInput.reloadModel();
const model = assertIsDefined(this.modelService.getModel(newInput.resource));
this.searchResultEditor.setModel(model); 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.hideHeader();
this.pauseSearching = true; this.pauseSearching = true;
this.queryEditorWidget.setValue(newInput.config.query, true); const { query } = await newInput.reloadModel();
this.queryEditorWidget.searchInput.setCaseSensitive(newInput.config.caseSensitive);
this.queryEditorWidget.searchInput.setRegex(newInput.config.regexp); this.queryEditorWidget.setValue(query.query, true);
this.queryEditorWidget.searchInput.setWholeWords(newInput.config.wholeWord); this.queryEditorWidget.searchInput.setCaseSensitive(query.caseSensitive);
this.queryEditorWidget.setContextLines(newInput.config.contextLines); this.queryEditorWidget.searchInput.setRegex(query.regexp);
this.inputPatternExcludes.setValue(newInput.config.excludes); this.queryEditorWidget.searchInput.setWholeWords(query.wholeWord);
this.inputPatternIncludes.setValue(newInput.config.includes); this.queryEditorWidget.setContextLines(query.contextLines);
this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(newInput.config.useIgnores); this.inputPatternExcludes.setValue(query.excludes);
this.toggleIncludesExcludes(newInput.config.showIncludesExcludes); this.inputPatternIncludes.setValue(query.includes);
this.inputPatternExcludes.setUseExcludesAndIgnoreFiles(query.useIgnores);
this.toggleIncludesExcludes(query.showIncludesExcludes);
this.focusInput(); this.focusInput();
this.pauseSearching = false; this.pauseSearching = false;
...@@ -415,3 +408,12 @@ export class SearchEditor extends BaseEditor { ...@@ -415,3 +408,12 @@ export class SearchEditor extends BaseEditor {
this.inSearchEditorContextKey.set(false); 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 } })));
};
...@@ -3,28 +3,17 @@ ...@@ -3,28 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import { coalesce, flatten } from 'vs/base/common/arrays';
import * as network from 'vs/base/common/network'; import * as network from 'vs/base/common/network';
import { repeat, endsWith } from 'vs/base/common/strings'; import { endsWith } from 'vs/base/common/strings';
import { assertIsDefined } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/searchEditor'; import 'vs/css!./media/searchEditor';
import { isDiffEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModel, ITextBufferFactory } from 'vs/editor/common/model';
import { Range } from 'vs/editor/common/core/range';
import { EndOfLinePreference, TrackedRangeStickiness, ITextModel, ITextBuffer, DefaultEndOfLine } from 'vs/editor/common/model';
import { localize } from 'vs/nls'; import { localize } from 'vs/nls';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ILabelService } from 'vs/platform/label/common/label';
import { searchEditorFindMatch, searchEditorFindMatchBorder } from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput';
import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextQuery } from 'vs/workbench/services/search/common/search';
import { IEditorInputFactory, GroupIdentifier, EditorInput, SaveContext, IRevertOptions } from 'vs/workbench/common/editor'; import { IEditorInputFactory, GroupIdentifier, EditorInput, SaveContext, IRevertOptions } from 'vs/workbench/common/editor';
import { IModelService } from 'vs/editor/common/services/modelService'; import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService'; import { IModeService } from 'vs/editor/common/services/modeService';
import { SearchEditor } from 'vs/workbench/contrib/search/browser/searchEditor';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import type { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import type { IWorkbenchContribution } from 'vs/workbench/common/contributions';
...@@ -36,8 +25,8 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; ...@@ -36,8 +25,8 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { basename } from 'vs/base/common/path'; import { basename } from 'vs/base/common/path';
import { IWorkingCopyService, WorkingCopyCapabilities, IWorkingCopy, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopyService, WorkingCopyCapabilities, IWorkingCopy, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { assertIsDefined } from 'vs/base/common/types';
import { extractSearchQuery, serializeSearchConfiguration } from 'vs/workbench/contrib/search/browser/searchEditorSerialization';
export type SearchConfiguration = { export type SearchConfiguration = {
query: string, query: string,
...@@ -51,93 +40,17 @@ export type SearchConfiguration = { ...@@ -51,93 +40,17 @@ export type SearchConfiguration = {
showIncludesExcludes: boolean, showIncludesExcludes: boolean,
}; };
export class SearchEditorContribution implements IWorkbenchContribution {
constructor(
@IEditorService private readonly editorService: IEditorService,
@ITextFileService protected readonly textFileService: ITextFileService,
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@IModelService protected readonly modelService: IModelService,
) {
this.editorService.overrideOpenEditor((editor, options, group) => {
const resource = editor.getResource();
if (!resource ||
!(endsWith(resource.path, '.code-search') || resource.scheme === 'search-editor') ||
!(editor instanceof FileEditorInput || (resource.scheme === 'search-editor'))) {
return undefined;
}
if (group.isOpened(editor)) {
return undefined;
}
return {
override: (async () => {
const contents = resource.scheme === 'search-editor' ? this.modelService.getModel(resource)?.getValue() ?? '' : (await this.textFileService.read(resource)).value;
const header = searchHeaderToContentPattern(contents.split('\n').slice(0, 5));
const input = instantiationService.createInstance(
SearchEditorInput,
{
query: header.pattern,
regexp: header.flags.regex,
caseSensitive: header.flags.caseSensitive,
wholeWord: header.flags.wholeWord,
includes: header.includes,
excludes: header.excludes,
contextLines: header.context ?? 0,
useIgnores: !header.flags.ignoreExcludes,
showIncludesExcludes: !!(header.includes || header.excludes || header.flags.ignoreExcludes)
}, contents, resource);
return editorService.openEditor(input, { ...options, pinned: resource.scheme === 'search-editor', ignoreOverrides: true }, group);
})()
};
});
}
}
export class SearchEditorInputFactory implements IEditorInputFactory {
canSerialize() { return true; }
serialize(input: SearchEditorInput) {
let resource = undefined;
if (input.resource.path || input.resource.fragment) {
resource = input.resource.toString();
}
return JSON.stringify({ ...input.config, resource, dirty: input.isDirty() });
}
deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): SearchEditorInput | undefined {
const { resource, dirty, ...config } = JSON.parse(serializedEditorInput);
const input = instantiationService.createInstance(SearchEditorInput, config, undefined, resource && URI.parse(resource));
input.setDirty(dirty);
return input;
}
}
export class SearchEditorInput extends EditorInput { export class SearchEditorInput extends EditorInput {
static readonly ID: string = 'workbench.editorinputs.searchEditorInput'; static readonly ID: string = 'workbench.editorinputs.searchEditorInput';
private _config: SearchConfiguration;
public get config(): Readonly<SearchConfiguration> {
return this._config;
}
private model: ITextModel;
public readonly resource: URI;
private dirty: boolean = false; private dirty: boolean = false;
private hasRestoredFromBackup = false; private readonly model: Promise<ITextModel>;
private resolvedModel?: { model: ITextModel, query: SearchConfiguration };
constructor( constructor(
config: Partial<SearchConfiguration> | undefined, public readonly resource: URI,
initialContents: string | undefined, getModel: () => Promise<ITextModel>,
resource: URI | undefined,
@IModelService private readonly modelService: IModelService, @IModelService private readonly modelService: IModelService,
@IModeService private readonly modeService: IModeService,
@IEditorService protected readonly editorService: IEditorService, @IEditorService protected readonly editorService: IEditorService,
@IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService,
@ITextFileService protected readonly textFileService: ITextFileService, @ITextFileService protected readonly textFileService: ITextFileService,
...@@ -146,15 +59,10 @@ export class SearchEditorInput extends EditorInput { ...@@ -146,15 +59,10 @@ export class SearchEditorInput extends EditorInput {
@IFileDialogService private readonly fileDialogService: IFileDialogService, @IFileDialogService private readonly fileDialogService: IFileDialogService,
@IInstantiationService private readonly instantiationService: IInstantiationService, @IInstantiationService private readonly instantiationService: IInstantiationService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
@IBackupFileService private readonly backupService: IBackupFileService,
) { ) {
super(); super();
this.resource = resource ?? URI.from({ scheme: 'search-editor', fragment: `${Math.random()}` });
this._config = { ...{ query: '', includes: '', excludes: '', contextLines: 0, wholeWord: false, caseSensitive: false, regexp: false, useIgnores: true, showIncludesExcludes: false }, ...config };
const searchResultMode = this.modeService.create('search-result'); this.model = getModel();
this.model = this.modelService.getModel(this.resource) ?? this.modelService.createModel(initialContents ?? '', searchResultMode, this.resource);
const workingCopyAdapter: IWorkingCopy = { const workingCopyAdapter: IWorkingCopy = {
resource: this.resource, resource: this.resource,
...@@ -167,18 +75,17 @@ export class SearchEditorInput extends EditorInput { ...@@ -167,18 +75,17 @@ export class SearchEditorInput extends EditorInput {
revert: () => this.revert(), revert: () => this.revert(),
}; };
this.workingCopyService.registerWorkingCopy(workingCopyAdapter); this.workingCopyService.registerWorkingCopy(workingCopyAdapter);
} }
async save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise<boolean> { async save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise<boolean> {
if (this.resource.scheme === 'search-editor') { if (this.resource.scheme === 'search-editor') {
const path = await this.promptForPath(this.resource, this.suggestFileName(), options?.availableFileSystems); const path = await this.promptForPath(this.resource, await this.suggestFileName(), options?.availableFileSystems);
if (path) { if (path) {
if (await this.textFileService.saveAs(this.resource, path, options)) { if (await this.textFileService.saveAs(this.resource, path, options)) {
this.setDirty(false); this.setDirty(false);
if (options?.context !== SaveContext.EDITOR_CLOSE && !isEqual(path, this.resource)) { if (options?.context !== SaveContext.EDITOR_CLOSE && !isEqual(path, this.resource)) {
const replacement = this.instantiationService.createInstance(SearchEditorInput, this.config, undefined, path); const replacement = this.instantiationService.invokeFunction(getOrMakeSearchEditorInput, { uri: path });
await this.editorService.replaceEditors([{ editor: this, replacement, options: { pinned: true } }], group); await this.editorService.replaceEditors([{ editor: this, replacement, options: { pinned: true } }], group);
return true; return true;
} else if (options?.context === SaveContext.EDITOR_CLOSE) { } else if (options?.context === SaveContext.EDITOR_CLOSE) {
...@@ -189,7 +96,7 @@ export class SearchEditorInput extends EditorInput { ...@@ -189,7 +96,7 @@ export class SearchEditorInput extends EditorInput {
return false; return false;
} else { } else {
this.setDirty(false); this.setDirty(false);
return !!this.textFileService.write(this.resource, this.model.getValue(), options); return !!this.textFileService.write(this.resource, (await this.model).getValue(), options);
} }
} }
...@@ -207,24 +114,32 @@ export class SearchEditorInput extends EditorInput { ...@@ -207,24 +114,32 @@ export class SearchEditorInput extends EditorInput {
getName(): string { getName(): string {
if (this.resource.scheme === 'search-editor') { if (this.resource.scheme === 'search-editor') {
return this.config.query ? localize('searchTitle.withQuery', "Search: {0}", this.config.query) : localize('searchTitle', "Search"); return this.resolvedModel?.query.query
? localize('searchTitle.withQuery', "Search: {0}", this.resolvedModel?.query.query)
: localize('searchTitle', "Search");
} }
return localize('searchTitle.withQuery', "Search: {0}", basename(this.resource.path, '.code-search')); return localize('searchTitle.withQuery', "Search: {0}", basename(this.resource.path, '.code-search'));
} }
setConfig(config: SearchConfiguration) { async reloadModel() {
this._config = config; const model = await this.model;
const query = extractSearchQuery(model);
this.resolvedModel = { model, query };
this._onDidChangeLabel.fire(); this._onDidChangeLabel.fire();
return { model, query };
} }
async resolve() { getConfigSync() {
return null; if (!this.resolvedModel) {
console.error('Requested config for Search Editor before initalization');
}
return this.resolvedModel?.query;
} }
async resolveBackup(): Promise<ITextBuffer | undefined> { async resolve() {
if (this.hasRestoredFromBackup === true) { return undefined; } return null;
this.hasRestoredFromBackup = true;
return (await this.backupService.resolve(this.resource))?.value.create(DefaultEndOfLine.LF);
} }
setDirty(dirty: boolean) { setDirty(dirty: boolean) {
...@@ -263,20 +178,16 @@ export class SearchEditorInput extends EditorInput { ...@@ -263,20 +178,16 @@ export class SearchEditorInput extends EditorInput {
} }
private async backup(): Promise<IWorkingCopyBackup> { private async backup(): Promise<IWorkingCopyBackup> {
if (this.model.isDisposed() || this.model.getValueLength() === 0) { const content = (await this.model).createSnapshot();
// FIXME: this is clearly not good, but `backup` is sometimes getting called after the return { content };
// model disposes, so we cant reliably grab the snapshot from the model. Instead fall back to the existing snapshot, if one exists.
// Ideally we'd return undefined and thus signal we dont want to overwrite any existing backup.
return { content: (await this.backupService.resolve(this.resource))?.value.create(DefaultEndOfLine.LF).createSnapshot(true) };
}
return { content: this.model.createSnapshot() };
} }
// Bringing this over from textFileService because it only suggests for untitled scheme. // Bringing this over from textFileService because it only suggests for untitled scheme.
// In the future I may just use the untitled scheme. I dont get particular benefit from using search-editor... // In the future I may just use the untitled scheme. I dont get particular benefit from using search-editor...
private suggestFileName(): URI { private async suggestFileName(): Promise<URI> {
const searchFileName = (this.config.query.replace(/[^\w \-_]+/g, '_') || 'Search') + '.code-search'; const query = (await this.reloadModel()).query.query;
const searchFileName = (query.replace(/[^\w \-_]+/g, '_') || 'Search') + '.code-search';
const remoteAuthority = this.environmentService.configuration.remoteAuthority; const remoteAuthority = this.environmentService.configuration.remoteAuthority;
const schemeFilter = remoteAuthority ? network.Schemas.vscodeRemote : network.Schemas.file; const schemeFilter = remoteAuthority ? network.Schemas.vscodeRemote : network.Schemas.file;
...@@ -296,291 +207,103 @@ export class SearchEditorInput extends EditorInput { ...@@ -296,291 +207,103 @@ export class SearchEditorInput extends EditorInput {
} }
} }
// 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] }); export class SearchEditorContribution implements IWorkbenchContribution {
}); constructor(
@IEditorService private readonly editorService: IEditorService,
return results; @ITextFileService protected readonly textFileService: ITextFileService,
}; @IInstantiationService protected readonly instantiationService: IInstantiationService,
@IModelService protected readonly modelService: IModelService,
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>(); this.editorService.overrideOpenEditor((editor, options, group) => {
serializedMatches.forEach(match => { const resource = editor.getResource();
if (!seenLines.has(match.line)) { if (!resource ||
while (context.length && context[0].lineNumber < +match.lineNumber) { !(endsWith(resource.path, '.code-search') || resource.scheme === 'search-editor') ||
const { line, lineNumber } = context.shift()!; !(editor instanceof FileEditorInput || (resource.scheme === 'search-editor'))) {
if (lastLine !== undefined && lineNumber !== lastLine + 1) { return undefined;
text.push('');
}
text.push(` ${lineNumber} ${line}`);
lastLine = lineNumber;
} }
targetLineNumberToOffset[match.lineNumber] = text.length; if (group.isOpened(editor)) {
seenLines.add(match.line); return undefined;
text.push(match.line);
lastLine = +match.lineNumber;
} }
matchRanges.push(...match.ranges.map(translateRangeLines(targetLineNumberToOffset[match.lineNumber]))); const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { uri: resource });
const opened = editorService.openEditor(input, { ...options, pinned: resource.scheme === 'search-editor', ignoreOverrides: true }, group);
return { override: Promise.resolve(opened) };
}); });
while (context.length) {
const { line, lineNumber } = context.shift()!;
text.push(` ${lineNumber} ${line}`);
} }
return { text, matchRanges };
} }
const flattenSearchResultSerializations = (serializations: SearchResultSerialization[]): SearchResultSerialization => { export class SearchEditorInputFactory implements IEditorInputFactory {
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 }; canSerialize() { return true; }
};
const contentPatternToSearchResultHeader = (pattern: ITextQuery | null, includes: string, excludes: string, contextLines: number): string[] => { serialize(input: SearchEditorInput) {
if (!pattern) { return []; } let resource = undefined;
if (input.resource.path || input.resource.fragment) {
resource = input.resource.toString();
}
const removeNullFalseAndUndefined = <T>(a: (T | null | false | undefined)[]) => a.filter(a => a !== false && a !== null && a !== undefined) as T[]; const config = input.getConfigSync();
const escapeNewlines = (str: string) => str.replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); return JSON.stringify({ resource, dirty: input.isDirty(), config });
}
return removeNullFalseAndUndefined([ deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): SearchEditorInput | undefined {
`# Query: ${escapeNewlines(pattern.contentPattern.pattern)}`, const { resource, dirty, config } = JSON.parse(serializedEditorInput);
(pattern.contentPattern.isCaseSensitive || pattern.contentPattern.isWordMatch || pattern.contentPattern.isRegExp || pattern.userDisabledExcludesAndIgnoreFiles) const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { text: serializeSearchConfiguration(config), uri: URI.parse(resource) });
&& `# Flags: ${coalesce([ input.setDirty(dirty);
pattern.contentPattern.isCaseSensitive && 'CaseSensitive', return input;
pattern.contentPattern.isWordMatch && 'WordMatch', }
pattern.contentPattern.isRegExp && 'RegExp', }
pattern.userDisabledExcludesAndIgnoreFiles && 'IgnoreExcludeSettings'
]).join(' ')}`,
includes ? `# Including: ${includes}` : undefined,
excludes ? `# Excluding: ${excludes}` : undefined,
contextLines ? `# ContextLines: ${contextLines}` : undefined,
''
]);
};
type SearchHeader = { const inputs = new Map<string, SearchEditorInput>();
pattern: string; export const getOrMakeSearchEditorInput = (
flags: { accessor: ServicesAccessor,
regex: boolean; existingData: { uri: URI, text?: string } | { text: string, uri?: URI }
wholeWord: boolean; ): SearchEditorInput => {
caseSensitive: boolean;
ignoreExcludes: boolean;
};
includes: string;
excludes: string;
context: number | undefined;
};
const searchHeaderToContentPattern = (header: string[]): SearchHeader => { const uri = existingData.uri ?? URI.from({ scheme: 'search-editor', fragment: `${Math.random()}` });
const query: SearchHeader = {
pattern: '',
flags: { regex: false, caseSensitive: false, ignoreExcludes: false, wholeWord: false },
includes: '',
excludes: '',
context: undefined
};
const unescapeNewlines = (str: string) => { const instantiationService = accessor.get(IInstantiationService);
let out = ''; const modelService = accessor.get(IModelService);
for (let i = 0; i < str.length; i++) { const textFileService = accessor.get(ITextFileService);
if (str[i] === '\\') { const backupService = accessor.get(IBackupFileService);
i++; const modeService = accessor.get(IModeService);
const escaped = str[i];
if (escaped === 'n') { const existing = inputs.get(uri.toString());
out += '\n'; if (existing) {
} return existing;
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.pattern = unescapeNewlines(value); break;
case 'Including': query.includes = value; break;
case 'Excluding': query.excludes = value; break;
case 'ContextLines': query.context = +value; break;
case 'Flags': {
query.flags = {
regex: value.indexOf('RegExp') !== -1,
caseSensitive: value.indexOf('CaseSensitive') !== -1,
ignoreExcludes: value.indexOf('IgnoreExcludeSettings') !== -1,
wholeWord: value.indexOf('WordMatch') !== -1
};
}
}
} }
return query;
};
export const serializeSearchResultForEditor = (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, contextLines: number, labelFormatter: (x: URI) => string, includeHeader: boolean): SearchResultSerialization => { const getModel = async () => {
const header = includeHeader ? contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines) : []; const existing = modelService.getModel(uri);
const allResults = if (existing) { return existing; }
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']) }; // must be called before `hasBackupSync` to ensure the backup service is initalized.
}; await backupService.getBackups();
export const openNewSearchEditor = let contents: string | ITextBufferFactory;
async (editorService: IEditorService, instantiationService: IInstantiationService) => { if (backupService.hasBackupSync(uri)) {
const activeEditor = editorService.activeTextEditorWidget; contents = assertIsDefined((await backupService.resolve(uri))?.value);
let activeModel: ICodeEditor | undefined; // backupService.discardBackup(uri);
if (isDiffEditor(activeEditor)) { } else if (uri.scheme !== 'search-editor') {
if (activeEditor.getOriginalEditor().hasTextFocus()) { contents = (await textFileService.read(uri)).value;
activeModel = activeEditor.getOriginalEditor();
} else { } else {
activeModel = activeEditor.getModifiedEditor(); contents = existingData.text ?? '';
} }
} else { return modelService.createModel(contents, modeService.create('search-result'), uri);
activeModel = activeEditor as ICodeEditor | undefined;
}
const selection = activeModel?.getSelection();
let selected = (selection && activeModel?.getModel()?.getValueInRange(selection)) ?? '';
await editorService.openEditor(instantiationService.createInstance(SearchEditorInput, { query: selected }, undefined, undefined), { pinned: true });
}; };
export const createEditorFromSearchResult = const input = instantiationService.createInstance(SearchEditorInput, uri, getModel);
async (searchResult: SearchResult, rawIncludePattern: string, rawExcludePattern: string, labelService: ILabelService, editorService: IEditorService, textFileService: ITextFileService, instantiationService: IInstantiationService) => {
if (!searchResult.query) {
console.error('Expected searchResult.query to be defined. Got', searchResult);
return;
}
const searchTerm = searchResult.query.contentPattern.pattern.replace(/[^\w-_. ]/g, '') || 'Search';
const labelFormatter = (uri: URI): string => labelService.getUriLabel(uri, { relative: true }); inputs.set(uri.toString(), input);
input.onDispose(() => inputs.delete(uri.toString()));
const results = serializeSearchResultForEditor(searchResult, rawIncludePattern, rawExcludePattern, 0, labelFormatter, true); return input;
const contents = results.text.join(lineDelimiter); };
let possible = {
contents,
mode: 'search-result',
resource: URI.from({ scheme: network.Schemas.untitled, path: searchTerm })
};
let id = 0;
let existing = textFileService.untitled.get(possible.resource);
while (existing) {
if (existing instanceof UntitledTextEditorInput) {
const model = await existing.resolve();
const existingContents = model.textEditorModel.getValue(EndOfLinePreference.LF);
if (existingContents === contents) {
break;
}
}
possible.resource = possible.resource.with({ path: searchTerm + '-' + ++id });
existing = textFileService.untitled.get(possible.resource);
}
const input = instantiationService.createInstance(
SearchEditorInput,
{
query: searchResult.query.contentPattern.pattern,
regexp: !!searchResult.query.contentPattern.isRegExp,
caseSensitive: !!searchResult.query.contentPattern.isCaseSensitive,
wholeWord: !!searchResult.query.contentPattern.isWordMatch,
includes: rawIncludePattern,
excludes: rawExcludePattern,
contextLines: 0,
useIgnores: !searchResult.query.userDisabledExcludesAndIgnoreFiles,
showIncludesExcludes: !!(rawExcludePattern || rawExcludePattern || searchResult.query.userDisabledExcludesAndIgnoreFiles)
}, contents, undefined);
const editor = await editorService.openEditor(input, { pinned: true }) as SearchEditor;
const model = assertIsDefined(editor.getModel());
model.deltaDecorations([], results.matchRanges.map(range => ({ range, options: { className: 'searchEditorFindMatch', stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } })));
};
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 '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 ...@@ -43,7 +43,7 @@ import { OpenFileFolderAction, OpenFolderAction } from 'vs/workbench/browser/act
import { ResourceLabels } from 'vs/workbench/browser/labels'; import { ResourceLabels } from 'vs/workbench/browser/labels';
import { IEditor } from 'vs/workbench/common/editor'; import { IEditor } from 'vs/workbench/common/editor';
import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; 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 { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate, SearchDND } from 'vs/workbench/contrib/search/browser/searchResultsView';
import { ISearchWidgetOptions, SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import { ISearchWidgetOptions, SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget';
import * as Constants from 'vs/workbench/contrib/search/common/constants'; import * as Constants from 'vs/workbench/contrib/search/common/constants';
...@@ -64,7 +64,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; ...@@ -64,7 +64,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { MultiCursorSelectionController } from 'vs/editor/contrib/multicursor/multicursor'; import { MultiCursorSelectionController } from 'vs/editor/contrib/multicursor/multicursor';
import { Selection } from 'vs/editor/common/core/selection'; import { Selection } from 'vs/editor/common/core/selection';
import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; 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 { ILabelService } from 'vs/platform/label/common/label';
import { Color, RGBA } from 'vs/base/common/color'; import { Color, RGBA } from 'vs/base/common/color';
...@@ -114,7 +114,7 @@ export class SearchView extends ViewPane { ...@@ -114,7 +114,7 @@ export class SearchView extends ViewPane {
private state: SearchUIState = SearchUIState.Idle; private state: SearchUIState = SearchUIState.Idle;
private actions: Array<CollapseDeepestExpandedLevelAction | ClearSearchResultsAction | OpenResultsInEditorAction> = []; private actions: Array<CollapseDeepestExpandedLevelAction | ClearSearchResultsAction | OpenSearchEditorAction> = [];
private toggleCollapseAction: ToggleCollapseAndExpandAction; private toggleCollapseAction: ToggleCollapseAndExpandAction;
private cancelAction: CancelSearchAction; private cancelAction: CancelSearchAction;
private refreshAction: RefreshAction; private refreshAction: RefreshAction;
...@@ -232,7 +232,7 @@ export class SearchView extends ViewPane { ...@@ -232,7 +232,7 @@ export class SearchView extends ViewPane {
if (this.searchConfig.enableSearchEditorPreview) { if (this.searchConfig.enableSearchEditorPreview) {
this.actions.push( 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 { ...@@ -1559,7 +1559,7 @@ export class SearchView extends ViewPane {
this.messageDisposables.push(dom.addDisposableListener(openInEditorLink, dom.EventType.CLICK, (e: MouseEvent) => { this.messageDisposables.push(dom.addDisposableListener(openInEditorLink, dom.EventType.CLICK, (e: MouseEvent) => {
dom.EventHelper.stop(e, false); 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 { } else {
......
...@@ -9,14 +9,18 @@ import { Registry } from 'vs/platform/registry/common/platform'; ...@@ -9,14 +9,18 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { UserDataSycnUtilServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; 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 { class UserDataSyncServicesContribution implements IWorkbenchContribution {
constructor( constructor(
@IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService, @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService,
@ISharedProcessService sharedProcessService: ISharedProcessService, @ISharedProcessService sharedProcessService: ISharedProcessService,
@IGlobalExtensionEnablementService globalExtensionEnablementService: IGlobalExtensionEnablementService,
) { ) {
sharedProcessService.registerChannel('userDataSyncUtil', new UserDataSycnUtilServiceChannel(userDataSyncUtilService)); sharedProcessService.registerChannel('userDataSyncUtil', new UserDataSycnUtilServiceChannel(userDataSyncUtilService));
sharedProcessService.registerChannel('globalExtensionEnablement', new GlobalExtensionEnablementServiceChannel(globalExtensionEnablementService));
} }
} }
......
...@@ -28,6 +28,10 @@ export interface IBackupFileService { ...@@ -28,6 +28,10 @@ export interface IBackupFileService {
/** /**
* Finds out if the provided resource with the given version is backed up. * 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; hasBackupSync(resource: URI, versionId?: number): boolean;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册