diff --git a/extensions/package.json b/extensions/package.json index 4bf8ba58c98..a7edc088808 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "2.9.1-insiders.20180516" + "typescript": "2.9.1-insiders.20180521" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/search-rg/src/ripgrepTextSearch.ts b/extensions/search-rg/src/ripgrepTextSearch.ts index 841bfac8730..edb1d1dbf1c 100644 --- a/extensions/search-rg/src/ripgrepTextSearch.ts +++ b/extensions/search-rg/src/ripgrepTextSearch.ts @@ -55,17 +55,20 @@ export class RipgrepTextSearchEngine { const escapedArgs = rgArgs .map(arg => arg.match(/^-/) ? arg : `'${arg}'`) .join(' '); - this.outputChannel.appendLine(`rg ${escapedArgs}\n - cwd: ${cwd}\n`); + this.outputChannel.appendLine(`rg ${escapedArgs}\n - cwd: ${cwd}`); this.rgProc = cp.spawn(rgDiskPath, rgArgs, { cwd }); process.once('exit', this.killRgProcFn); this.rgProc.on('error', e => { - console.log(e); + console.error(e); + this.outputChannel.append('Error: ' + (e && e.message)); reject(e); }); + let gotResult = false; this.ripgrepParser = new RipgrepParser(MAX_TEXT_RESULTS, cwd); this.ripgrepParser.on('result', (match: vscode.TextSearchResult) => { + gotResult = true; progress.report(match); }); @@ -83,11 +86,14 @@ export class RipgrepTextSearchEngine { let stderr = ''; this.rgProc.stderr.on('data', data => { const message = data.toString(); - this.outputChannel.appendLine(message); + this.outputChannel.append(message); stderr += message; }); this.rgProc.on('close', code => { + this.outputChannel.appendLine(gotData ? 'Got data from stdout' : 'No data from stdout'); + this.outputChannel.appendLine(gotResult ? 'Got result from parser' : 'No result from parser'); + this.outputChannel.appendLine(''); process.removeListener('exit', this.killRgProcFn); if (this.isDone) { resolve(); diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index a834d698627..a218c8253ce 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -482,6 +482,28 @@ "default": true, "description": "%typescript.showUnused.enabled%", "scope": "resource" + }, + "typescript.updateImportsOnFileMove.enabled": { + "type": "string", + "enum": [ + "prompt", + "always", + "never" + ], + "default": "prompt", + "description": "%typescript.updateImportsOnFileMove.enabled%", + "scope": "resource" + }, + "javascript.updateImportsOnFileMove.enabled": { + "type": "string", + "enum": [ + "prompt", + "always", + "never" + ], + "default": "prompt", + "description": "%typescript.updateImportsOnFileMove.enabled%", + "scope": "resource" } } }, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 66b199464c2..e23a7c863de 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -57,5 +57,6 @@ "typescript.suggestionActions.enabled": "Enable/disable suggestion diagnostics for TypeScript files in the editor. Requires TypeScript >= 2.8", "typescript.preferences.quoteStyle": "Preferred quote style to use for quick fixes: 'single' quotes, 'double' quotes, or 'auto' infer quote type from existing imports. Requires TS >= 2.9", "typescript.preferences.importModuleSpecifier": "Preferred path style for auto imports: 'relative' paths, 'non-relative' paths, or 'auto' infer the shortest path type. Requires TS >= 2.9", - "typescript.showUnused.enabled": "Enable/disable highlighting of unused variables in code. Requires TypeScript >= 2.9" + "typescript.showUnused.enabled": "Enable/disable highlighting of unused variables in code. Requires TypeScript >= 2.9", + "typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code. Possible values are: 'prompt' on each rename, 'always' update paths automatically, and 'never' rename paths and don't prompt me. Requires TypeScript >= 2.9" } \ No newline at end of file diff --git a/extensions/typescript-language-features/src/features/bufferSyncSupport.ts b/extensions/typescript-language-features/src/features/bufferSyncSupport.ts index 2a1120a0be1..554dfc22d23 100644 --- a/extensions/typescript-language-features/src/features/bufferSyncSupport.ts +++ b/extensions/typescript-language-features/src/features/bufferSyncSupport.ts @@ -172,10 +172,10 @@ export default class BufferSyncSupport { } public listen(): void { - workspace.onDidOpenTextDocument(this.onDidOpenTextDocument, this, this.disposables); + workspace.onDidOpenTextDocument(this.openTextDocument, this, this.disposables); workspace.onDidCloseTextDocument(this.onDidCloseTextDocument, this, this.disposables); workspace.onDidChangeTextDocument(this.onDidChangeTextDocument, this, this.disposables); - workspace.textDocuments.forEach(this.onDidOpenTextDocument, this); + workspace.textDocuments.forEach(this.openTextDocument, this); } public set validate(value: boolean) { @@ -196,7 +196,7 @@ export default class BufferSyncSupport { disposeAll(this.disposables); } - private onDidOpenTextDocument(document: TextDocument): void { + public openTextDocument(document: TextDocument): void { if (!this.modeIds.has(document.languageId)) { return; } diff --git a/extensions/typescript-language-features/src/features/documentSymbolProvider.ts b/extensions/typescript-language-features/src/features/documentSymbolProvider.ts index e81ebf49584..a30b0d3c3fc 100644 --- a/extensions/typescript-language-features/src/features/documentSymbolProvider.ts +++ b/extensions/typescript-language-features/src/features/documentSymbolProvider.ts @@ -100,9 +100,10 @@ export default class TypeScriptDocumentSymbolProvider implements DocumentSymbolP const hierarchy = new Hierarchy(symbolInfo); let shouldInclude = TypeScriptDocumentSymbolProvider.shouldInclueEntry(item); - if (item.childItems && item.childItems.length > 0) { + if (item.childItems) { for (const child of item.childItems) { - shouldInclude = shouldInclude || TypeScriptDocumentSymbolProvider.convertNavTree(resource, hierarchy.children, child); + const includedChild = TypeScriptDocumentSymbolProvider.convertNavTree(resource, hierarchy.children, child); + shouldInclude = shouldInclude || includedChild; } } diff --git a/extensions/typescript-language-features/src/features/refactorProvider.ts b/extensions/typescript-language-features/src/features/refactorProvider.ts index b206a128994..a12f41f298e 100644 --- a/extensions/typescript-language-features/src/features/refactorProvider.ts +++ b/extensions/typescript-language-features/src/features/refactorProvider.ts @@ -126,7 +126,7 @@ export default class TypeScriptRefactorProvider implements vscode.CodeActionProv return []; } - if (!(rangeOrSelection instanceof vscode.Selection) || rangeOrSelection.isEmpty) { + if (!(rangeOrSelection instanceof vscode.Selection) || (rangeOrSelection.isEmpty && context.triggerKind !== vscode.CodeActionTrigger.Manual)) { return []; } diff --git a/extensions/typescript-language-features/src/features/updatePathsOnRename.ts b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts new file mode 100644 index 00000000000..791baff7567 --- /dev/null +++ b/extensions/typescript-language-features/src/features/updatePathsOnRename.ts @@ -0,0 +1,206 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import * as Proto from '../protocol'; +import { ITypeScriptServiceClient } from '../typescriptService'; +import * as languageIds from '../utils/languageModeIds'; +import * as typeConverters from '../utils/typeConverters'; +import BufferSyncSupport from './bufferSyncSupport'; +import FileConfigurationManager from './fileConfigurationManager'; + +const localize = nls.loadMessageBundle(); + +const updateImportsOnFileMoveName = 'updateImportsOnFileMove.enabled'; + +enum UpdateImportsOnFileMoveSetting { + Prompt = 'prompt', + Always = 'always', + Never = 'never', +} + +export class UpdateImportsOnFileRenameHandler { + private readonly _onDidRenameSub: vscode.Disposable; + + public constructor( + private readonly client: ITypeScriptServiceClient, + private readonly bufferSyncSupport: BufferSyncSupport, + private readonly fileConfigurationManager: FileConfigurationManager, + private readonly handles: (uri: vscode.Uri) => Promise, + ) { + this._onDidRenameSub = vscode.workspace.onDidRenameResource(e => { + this.doRename(e.oldResource, e.newResource); + }); + } + + public dispose() { + this._onDidRenameSub.dispose(); + } + + private async doRename( + oldResource: vscode.Uri, + newResource: vscode.Uri, + ): Promise { + if (!this.client.apiVersion.has290Features) { + return; + } + + if (!await this.handles(newResource)) { + return; + } + + const newFile = this.client.normalizePath(newResource); + if (!newFile) { + return; + } + + const oldFile = this.client.normalizePath(oldResource); + if (!oldFile) { + return; + } + + const document = await vscode.workspace.openTextDocument(newResource); + + const config = this.getConfiguration(document); + const setting = config.get(updateImportsOnFileMoveName); + if (setting === UpdateImportsOnFileMoveSetting.Never) { + return; + } + + // Make sure TS knows about file + this.bufferSyncSupport.openTextDocument(document); + + const edits = await this.getEditsForFileRename(document, oldFile, newFile); + if (!edits || !edits.size) { + return; + } + + if (await this.confirmActionWithUser(document)) { + await vscode.workspace.applyEdit(edits); + } + } + + private async confirmActionWithUser( + newDocument: vscode.TextDocument + ): Promise { + const config = this.getConfiguration(newDocument); + const setting = config.get(updateImportsOnFileMoveName); + switch (setting) { + case UpdateImportsOnFileMoveSetting.Always: + return true; + case UpdateImportsOnFileMoveSetting.Never: + return false; + case UpdateImportsOnFileMoveSetting.Prompt: + default: + return this.promptUser(newDocument); + } + } + + private getConfiguration(newDocument: vscode.TextDocument) { + return vscode.workspace.getConfiguration(isTypeScriptDocument(newDocument) ? 'typescript' : 'javascript', newDocument.uri); + } + + private async promptUser( + newDocument: vscode.TextDocument + ): Promise { + enum Choice { + None = 0, + Accept = 1, + Reject = 2, + Always = 3, + Never = 4, + } + + interface Item extends vscode.QuickPickItem { + choice: Choice; + } + + const response = await vscode.window.showQuickPick([ + { + label: localize('accept.label', "Yes"), + description: localize('accept.description', "Update imports."), + choice: Choice.Accept, + }, + { + label: localize('reject.label', "No"), + description: localize('reject.description', "Do not update imports."), + choice: Choice.Reject, + }, + { + label: localize('always.label', "Always"), + description: localize('always.description', "Yes, and always automatically update imports."), + choice: Choice.Always, + }, + { + label: localize('never.label', "Never"), + description: localize('never.description', "No, and do not prompt me again."), + choice: Choice.Never, + }, + ], { + placeHolder: localize('prompt', "Update import paths?"), + ignoreFocusOut: true, + }); + + if (!response) { + return false; + } + + switch (response.choice) { + case Choice.Accept: + { + return true; + } + case Choice.Reject: + { + return false; + } + case Choice.Always: + { + const config = this.getConfiguration(newDocument); + config.update( + updateImportsOnFileMoveName, + UpdateImportsOnFileMoveSetting.Always, + vscode.ConfigurationTarget.Global); + return true; + } + case Choice.Never: + { + const config = this.getConfiguration(newDocument); + config.update( + updateImportsOnFileMoveName, + UpdateImportsOnFileMoveSetting.Never, + vscode.ConfigurationTarget.Global); + return false; + } + } + + return false; + } + + private async getEditsForFileRename( + document: vscode.TextDocument, + oldFile: string, + newFile: string, + ) { + await this.fileConfigurationManager.ensureConfigurationForDocument(document, undefined); + + const args: Proto.GetEditsForFileRenameRequestArgs = { + file: newFile, + oldFilePath: oldFile, + newFilePath: newFile, + }; + const response = await this.client.execute('getEditsForFileRename', args); + if (!response || !response.body) { + return; + } + + return typeConverters.WorkspaceEdit.fromFromFileCodeEdits(this.client, response.body); + } +} + +function isTypeScriptDocument(document: vscode.TextDocument) { + return document.languageId === languageIds.typescript || document.languageId === languageIds.typescriptreact; +} diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 312921c5671..7faf5fd5216 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { languages, workspace, Diagnostic, Disposable, Uri, TextDocument, DocumentFilter } from 'vscode'; +import { languages, workspace, Diagnostic, Disposable, Uri, TextDocument, DocumentFilter, DiagnosticSeverity } from 'vscode'; import { basename } from 'path'; import TypeScriptServiceClient from './typescriptServiceClient'; @@ -21,6 +21,7 @@ import { CachedNavTreeResponse } from './features/baseCodeLensProvider'; import { memoize } from './utils/memoize'; import { disposeAll } from './utils/dispose'; import TelemetryReporter from './utils/telemetry'; +import { UpdateImportsOnFileRenameHandler } from './features/updatePathsOnRename'; const validateSetting = 'validate.enable'; const suggestionSetting = 'suggestionActions.enabled'; @@ -40,6 +41,7 @@ export default class LanguageProvider { private readonly versionDependentDisposables: Disposable[] = []; private foldingProviderRegistration: Disposable | undefined = void 0; + private readonly renameHandler: UpdateImportsOnFileRenameHandler; constructor( private readonly client: TypeScriptServiceClient, @@ -64,6 +66,15 @@ export default class LanguageProvider { await this.registerProviders(client, commandManager, typingsStatus); this.bufferSyncSupport.listen(); }); + + this.renameHandler = new UpdateImportsOnFileRenameHandler(this.client, this.bufferSyncSupport, this.fileConfigurationManager, async uri => { + try { + const doc = await workspace.openTextDocument(uri); + return this.handles(uri, doc); + } catch { + return false; + } + }); } public dispose(): void { @@ -73,6 +84,7 @@ export default class LanguageProvider { this.diagnosticsManager.dispose(); this.bufferSyncSupport.dispose(); this.fileConfigurationManager.dispose(); + this.renameHandler.dispose(); } @memoize @@ -261,7 +273,15 @@ export default class LanguageProvider { public diagnosticsReceived(diagnosticsKind: DiagnosticKind, file: Uri, diagnostics: (Diagnostic & { reportUnnecessary: any })[]): void { const config = workspace.getConfiguration(this.id, file); const reportUnnecessary = config.get('showUnused.enabled', true); - this.diagnosticsManager.diagnosticsReceived(diagnosticsKind, file, diagnostics.filter(diag => diag.reportUnnecessary ? reportUnnecessary : true)); + this.diagnosticsManager.diagnosticsReceived(diagnosticsKind, file, diagnostics.filter(diag => { + if (!reportUnnecessary) { + diag.customTags = undefined; + if (diag.reportUnnecessary && diag.severity === DiagnosticSeverity.Hint) { + return false; + } + } + return true; + })); } public configFileDiagnosticsReceived(file: Uri, diagnostics: Diagnostic[]): void { diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index d5c882111f4..be31d8e299f 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -58,6 +58,7 @@ export interface ITypeScriptServiceClient { execute(command: 'applyCodeActionCommand', args: Proto.ApplyCodeActionCommandRequestArgs, token?: CancellationToken): Promise; execute(command: 'organizeImports', args: Proto.OrganizeImportsRequestArgs, token?: CancellationToken): Promise; execute(command: 'getOutliningSpans', args: Proto.FileRequestArgs, token: CancellationToken): Promise; + execute(command: 'getEditsForFileRename', args: Proto.GetEditsForFileRenameRequestArgs): Promise; execute(command: string, args: any, expectedResult: boolean | CancellationToken, token?: CancellationToken): Promise; executeAsync(command: 'geterr', args: Proto.GeterrRequestArgs, token: CancellationToken): Promise; diff --git a/extensions/yarn.lock b/extensions/yarn.lock index e51ea6e7239..b0bbfbba7d4 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,6 +2,6 @@ # yarn lockfile v1 -typescript@2.9.1-insiders.20180516: - version "2.9.1-insiders.20180516" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.1-insiders.20180516.tgz#aab5261edb2c162c2d0c1754bb3092d4ff6efed0" +typescript@2.9.1-insiders.20180521: + version "2.9.1-insiders.20180521" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.1-insiders.20180521.tgz#37e9c05f00aa99864c3f66781e607ae0097c2b0a" diff --git a/package.json b/package.json index 34878ee1799..23514cd7969 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "vscode-nsfw": "1.0.17", "vscode-ripgrep": "^0.8.1", "vscode-textmate": "^3.3.3", - "vscode-xterm": "3.5.0-beta6", + "vscode-xterm": "3.5.0-beta8", "yauzl": "^2.9.1" }, "devDependencies": { diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 6b7ae998466..cce71452cd7 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -348,11 +348,20 @@ export interface CodeAction { kind?: string; } +/** + * @internal + */ +export enum CodeActionTrigger { + Automatic = 1, + Manual = 2, +} + /** * @internal */ export interface CodeActionContext { only?: string; + trigger: CodeActionTrigger; } /** diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index 4d2770b8260..c13ef19a29d 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -11,14 +11,15 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -import { CodeAction, CodeActionProviderRegistry, CodeActionContext } from 'vs/editor/common/modes'; +import { CodeAction, CodeActionProviderRegistry, CodeActionContext, CodeActionTrigger as CodeActionTriggerKind } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { CodeActionFilter, CodeActionKind } from './codeActionTrigger'; +import { CodeActionFilter, CodeActionKind, CodeActionTrigger } from './codeActionTrigger'; import { Selection } from 'vs/editor/common/core/selection'; -export function getCodeActions(model: ITextModel, rangeOrSelection: Range | Selection, filter?: CodeActionFilter): TPromise { +export function getCodeActions(model: ITextModel, rangeOrSelection: Range | Selection, trigger?: CodeActionTrigger): TPromise { const codeActionContext: CodeActionContext = { - only: filter && filter.kind ? filter.kind.value : undefined, + only: trigger && trigger.filter && trigger.filter.kind ? trigger.filter.kind.value : undefined, + trigger: trigger && trigger.type === 'manual' ? CodeActionTriggerKind.Manual : CodeActionTriggerKind.Automatic }; const promises = CodeActionProviderRegistry.all(model).map(support => { @@ -26,7 +27,7 @@ export function getCodeActions(model: ITextModel, rangeOrSelection: Range | Sele if (!Array.isArray(providedCodeActions)) { return []; } - return providedCodeActions.filter(action => isValidAction(filter, action)); + return providedCodeActions.filter(action => isValidAction(trigger && trigger.filter, action)); }, (err): CodeAction[] => { if (isPromiseCanceledError(err)) { throw err; diff --git a/src/vs/editor/contrib/codeAction/codeActionModel.ts b/src/vs/editor/contrib/codeAction/codeActionModel.ts index b5b4e8dd766..acbd68b1fa1 100644 --- a/src/vs/editor/contrib/codeAction/codeActionModel.ts +++ b/src/vs/editor/contrib/codeAction/codeActionModel.ts @@ -113,7 +113,7 @@ export class CodeActionOracle { const model = this._editor.getModel(); const markerRange = this._getRangeOfMarker(selection); const position = markerRange ? markerRange.getStartPosition() : selection.getStartPosition(); - const actions = getCodeActions(model, selection, trigger && trigger.filter); + const actions = getCodeActions(model, selection, trigger); if (this._progressService && trigger.type === 'manual') { this._progressService.showWhile(actions, 250); diff --git a/src/vs/editor/contrib/codeAction/test/codeAction.test.ts b/src/vs/editor/contrib/codeAction/test/codeAction.test.ts index 2592da92092..c2cc03ff9da 100644 --- a/src/vs/editor/contrib/codeAction/test/codeAction.test.ts +++ b/src/vs/editor/contrib/codeAction/test/codeAction.test.ts @@ -136,20 +136,20 @@ suite('CodeAction', () => { disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); { - const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a') }); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a') } }); assert.equal(actions.length, 2); assert.strictEqual(actions[0].title, 'a'); assert.strictEqual(actions[1].title, 'a.b'); } { - const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a.b') }); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a.b') } }); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'a.b'); } { - const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a.b.c') }); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a.b.c') } }); assert.equal(actions.length, 0); } }); @@ -165,7 +165,7 @@ suite('CodeAction', () => { disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); - const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: new CodeActionKind('a') }); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a') } }); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'a'); }); @@ -183,13 +183,13 @@ suite('CodeAction', () => { disposables.push(CodeActionProviderRegistry.register('fooLang', provider)); { - const actions = await getCodeActions(model, new Range(1, 1, 2, 1), {}); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto' }); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'b'); } { - const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { kind: CodeActionKind.Source, includeSourceActions: true }); + const actions = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: CodeActionKind.Source, includeSourceActions: true } }); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'a'); } diff --git a/src/vs/platform/search/common/search.ts b/src/vs/platform/search/common/search.ts index 6eda69df5e1..000bb944418 100644 --- a/src/vs/platform/search/common/search.ts +++ b/src/vs/platform/search/common/search.ts @@ -104,17 +104,12 @@ export interface IPatternInfo { isSmartCase?: boolean; } -export interface IFileMatch { +export interface IFileMatch { resource?: U; lineMatches?: ILineMatch[]; } -export interface IPathInFolder { - folderIdx: number; - relativePath: string; -} - -export type IRawFileMatch2 = IFileMatch; +export type IRawFileMatch2 = IFileMatch; export interface ILineMatch { preview: string; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index b2cd89aa620..b7c2346400e 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -640,4 +640,42 @@ declare module 'vscode' { } //#endregion + + //#region mjbvz: File rename events + export interface ResourceRenamedEvent { + readonly oldResource: Uri; + readonly newResource: Uri; + } + + export namespace workspace { + export const onDidRenameResource: Event; + } + //#endregion + + //#region mjbvz: Code action trigger + + /** + * How a [code action provider](#CodeActionProvider) was triggered + */ + export enum CodeActionTrigger { + /** + * Provider was triggered automatically by VS Code. + */ + Automatic = 1, + + /** + * User requested code actions. + */ + Manual = 2, + } + + interface CodeActionContext { + /** + * How the code action provider was triggered. + */ + triggerKind?: CodeActionTrigger; + } + + //#endregion + } diff --git a/src/vs/workbench/api/electron-browser/mainThreadDocuments.ts b/src/vs/workbench/api/electron-browser/mainThreadDocuments.ts index 4474c00ee09..a85c61c9d4e 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDocuments.ts @@ -10,7 +10,7 @@ import { IModelService, shouldSynchronizeModel } from 'vs/editor/common/services import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; import { TextFileModelChangeEvent, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IFileService } from 'vs/platform/files/common/files'; +import { IFileService, FileOperation } from 'vs/platform/files/common/files'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { ExtHostContext, MainThreadDocumentsShape, ExtHostDocumentsShape, IExtHostContext } from '../node/extHost.protocol'; @@ -119,6 +119,12 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { } })); + this._toDispose.push(fileService.onAfterOperation(e => { + if (e.operation === FileOperation.MOVE) { + this._proxy.$onDidRename(e.resource, e.target.resource); + } + })); + this._modelToDisposeMap = Object.create(null); } diff --git a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts index eb74c54ffb1..6f10e6490be 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts @@ -307,7 +307,10 @@ class CodeActionOnParticipant implements ISaveParticipant { } private async getActionsToRun(model: ITextModel, codeActionsOnSave: CodeActionKind[]) { - const actions = await getCodeActions(model, model.getFullModelRange(), { kind: CodeActionKind.Source, includeSourceActions: true }); + const actions = await getCodeActions(model, model.getFullModelRange(), { + type: 'auto', + filter: { kind: CodeActionKind.Source, includeSourceActions: true }, + }); const actionsToRun = actions.filter(returnedAction => returnedAction.kind && codeActionsOnSave.some(onSaveKind => onSaveKind.contains(returnedAction.kind))); return actionsToRun; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadSearch.ts b/src/vs/workbench/api/electron-browser/mainThreadSearch.ts index 18a1adc73b2..1418afcb795 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSearch.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSearch.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as path from 'path'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { values } from 'vs/base/common/map'; import URI, { UriComponents } from 'vs/base/common/uri'; import { PPromise, TPromise } from 'vs/base/common/winjs.base'; -import { IFileMatch, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, QueryType, IRawFileMatch2, ISearchCompleteStats, IFolderQuery } from 'vs/platform/search/common/search'; +import { IFileMatch, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, QueryType, IRawFileMatch2, ISearchCompleteStats } from 'vs/platform/search/common/search'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { ExtHostContext, ExtHostSearchShape, IExtHostContext, MainContext, MainThreadSearchShape } from '../node/extHost.protocol'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -58,7 +57,6 @@ class SearchOperation { constructor( readonly progress: (match: IFileMatch) => any, - readonly folders: IFolderQuery[], readonly id: number = ++SearchOperation._idPool, readonly matches = new Map() ) { @@ -112,7 +110,7 @@ class RemoteSearchProvider implements ISearchResultProvider { return new PPromise((resolve, reject, report) => { - const search = new SearchOperation(report, query.folderQueries); + const search = new SearchOperation(report); this._searches.set(search.id, search); outer = query.type === QueryType.File @@ -142,14 +140,8 @@ class RemoteSearchProvider implements ISearchResultProvider { const searchOp = this._searches.get(session); if (Array.isArray(dataOrUri)) { dataOrUri.forEach(m => { - const folderQuery = searchOp.folders[m.resource.folderIdx]; - if (!folderQuery) { - return; - } - - const fullUri = URI.file(path.join(folderQuery.folder.fsPath, m.resource.relativePath)); searchOp.addMatch({ - resource: fullUri, + resource: URI.revive(m.resource), lineMatches: m.lineMatches }); }); diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index c4152220f51..ff1eec77d12 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -568,7 +568,10 @@ export function createApiFactory( }), registerSearchProvider: proposedApiFunction(extension, (scheme, provider) => { return extHostSearch.registerSearchProvider(scheme, provider); - }) + }), + onDidRenameResource: proposedApiFunction(extension, (listener, thisArg?, disposables?) => { + return extHostDocuments.onDidRenameResource(listener, thisArg, disposables); + }), }; // namespace: scm @@ -669,6 +672,7 @@ export function createApiFactory( Color: extHostTypes.Color, ColorPresentation: extHostTypes.ColorPresentation, ColorInformation: extHostTypes.ColorInformation, + CodeActionTrigger: extHostTypes.CodeActionTrigger, EndOfLine: extHostTypes.EndOfLine, CompletionItem: extHostTypes.CompletionItem, CompletionItemKind: extHostTypes.CompletionItemKind, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 03c1ea29e7f..809757316cb 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -530,6 +530,7 @@ export interface ExtHostDocumentsShape { $acceptModelSaved(strURL: UriComponents): void; $acceptDirtyStateChanged(strURL: UriComponents, isDirty: boolean): void; $acceptModelChanged(strURL: UriComponents, e: IModelChangedEvent, isDirty: boolean): void; + $onDidRename(oldURL: UriComponents, newURL: UriComponents): void; } export interface ExtHostDocumentSaveParticipantShape { diff --git a/src/vs/workbench/api/node/extHostDocuments.ts b/src/vs/workbench/api/node/extHostDocuments.ts index 7cc0f42359c..e9122c83d72 100644 --- a/src/vs/workbench/api/node/extHostDocuments.ts +++ b/src/vs/workbench/api/node/extHostDocuments.ts @@ -21,11 +21,13 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { private _onDidRemoveDocument = new Emitter(); private _onDidChangeDocument = new Emitter(); private _onDidSaveDocument = new Emitter(); + private _onDidRenameResource = new Emitter(); readonly onDidAddDocument: Event = this._onDidAddDocument.event; readonly onDidRemoveDocument: Event = this._onDidRemoveDocument.event; readonly onDidChangeDocument: Event = this._onDidChangeDocument.event; readonly onDidSaveDocument: Event = this._onDidSaveDocument.event; + readonly onDidRenameResource: Event = this._onDidRenameResource.event; private _toDispose: IDisposable[]; private _proxy: MainThreadDocumentsShape; @@ -148,4 +150,11 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { public setWordDefinitionFor(modeId: string, wordDefinition: RegExp): void { setWordDefinitionFor(modeId, wordDefinition); } + + public $onDidRename(oldURL: UriComponents, newURL: UriComponents): void { + const oldResource = URI.revive(oldURL); + const newResource = URI.revive(newURL); + this._onDidRenameResource.fire({ oldResource, newResource }); + } + } diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 2b777c8a1cd..a16de89cb21 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -88,17 +88,12 @@ class CodeLensAdapter { private static _badCmd: vscode.Command = { command: 'missing', title: '<>' }; - private _documents: ExtHostDocuments; - private _commands: CommandsConverter; - private _heapService: ExtHostHeapService; - private _provider: vscode.CodeLensProvider; - - constructor(documents: ExtHostDocuments, commands: CommandsConverter, heapService: ExtHostHeapService, provider: vscode.CodeLensProvider) { - this._documents = documents; - this._commands = commands; - this._heapService = heapService; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _commands: CommandsConverter, + private readonly _heapService: ExtHostHeapService, + private readonly _provider: vscode.CodeLensProvider + ) { } provideCodeLenses(resource: URI): TPromise { const doc = this._documents.getDocumentData(resource).document; @@ -140,13 +135,11 @@ class CodeLensAdapter { } class DefinitionAdapter { - private _documents: ExtHostDocuments; - private _provider: vscode.DefinitionProvider; - constructor(documents: ExtHostDocuments, provider: vscode.DefinitionProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.DefinitionProvider + ) { } provideDefinition(resource: URI, position: IPosition): TPromise { let doc = this._documents.getDocumentData(resource).document; @@ -163,13 +156,11 @@ class DefinitionAdapter { } class ImplementationAdapter { - private _documents: ExtHostDocuments; - private _provider: vscode.ImplementationProvider; - constructor(documents: ExtHostDocuments, provider: vscode.ImplementationProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.ImplementationProvider + ) { } provideImplementation(resource: URI, position: IPosition): TPromise { let doc = this._documents.getDocumentData(resource).document; @@ -186,13 +177,11 @@ class ImplementationAdapter { } class TypeDefinitionAdapter { - private _documents: ExtHostDocuments; - private _provider: vscode.TypeDefinitionProvider; - constructor(documents: ExtHostDocuments, provider: vscode.TypeDefinitionProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.TypeDefinitionProvider + ) { } provideTypeDefinition(resource: URI, position: IPosition): TPromise { const doc = this._documents.getDocumentData(resource).document; @@ -208,15 +197,12 @@ class TypeDefinitionAdapter { } } - class HoverAdapter { constructor( private readonly _documents: ExtHostDocuments, private readonly _provider: vscode.HoverProvider, - ) { - // - } + ) { } public provideHover(resource: URI, position: IPosition): TPromise { @@ -241,13 +227,10 @@ class HoverAdapter { class DocumentHighlightAdapter { - private _documents: ExtHostDocuments; - private _provider: vscode.DocumentHighlightProvider; - - constructor(documents: ExtHostDocuments, provider: vscode.DocumentHighlightProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.DocumentHighlightProvider + ) { } provideDocumentHighlights(resource: URI, position: IPosition): TPromise { @@ -265,13 +248,10 @@ class DocumentHighlightAdapter { class ReferenceAdapter { - private _documents: ExtHostDocuments; - private _provider: vscode.ReferenceProvider; - - constructor(documents: ExtHostDocuments, provider: vscode.ReferenceProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.ReferenceProvider + ) { } provideReferences(resource: URI, position: IPosition, context: modes.ReferenceContext): TPromise { let doc = this._documents.getDocumentData(resource).document; @@ -315,7 +295,8 @@ class CodeActionAdapter { const codeActionContext: vscode.CodeActionContext = { diagnostics: allDiagnostics, - only: context.only ? new CodeActionKind(context.only) : undefined + only: context.only ? new CodeActionKind(context.only) : undefined, + triggerKind: context.trigger, }; return asWinJsPromise(token => @@ -359,13 +340,10 @@ class CodeActionAdapter { class DocumentFormattingAdapter { - private _documents: ExtHostDocuments; - private _provider: vscode.DocumentFormattingEditProvider; - - constructor(documents: ExtHostDocuments, provider: vscode.DocumentFormattingEditProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.DocumentFormattingEditProvider + ) { } provideDocumentFormattingEdits(resource: URI, options: modes.FormattingOptions): TPromise { @@ -382,13 +360,10 @@ class DocumentFormattingAdapter { class RangeFormattingAdapter { - private _documents: ExtHostDocuments; - private _provider: vscode.DocumentRangeFormattingEditProvider; - - constructor(documents: ExtHostDocuments, provider: vscode.DocumentRangeFormattingEditProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.DocumentRangeFormattingEditProvider + ) { } provideDocumentRangeFormattingEdits(resource: URI, range: IRange, options: modes.FormattingOptions): TPromise { @@ -406,13 +381,10 @@ class RangeFormattingAdapter { class OnTypeFormattingAdapter { - private _documents: ExtHostDocuments; - private _provider: vscode.OnTypeFormattingEditProvider; - - constructor(documents: ExtHostDocuments, provider: vscode.OnTypeFormattingEditProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.OnTypeFormattingEditProvider + ) { } autoFormatTriggerCharacters: string[] = []; // not here @@ -498,13 +470,10 @@ class RenameAdapter { return typeof provider.prepareRename === 'function'; } - private _documents: ExtHostDocuments; - private _provider: vscode.RenameProvider; - - constructor(documents: ExtHostDocuments, provider: vscode.RenameProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.RenameProvider + ) { } provideRenameEdits(resource: URI, position: IPosition, newName: string): TPromise { @@ -733,13 +702,10 @@ class SuggestAdapter { class SignatureHelpAdapter { - private _documents: ExtHostDocuments; - private _provider: vscode.SignatureHelpProvider; - - constructor(documents: ExtHostDocuments, provider: vscode.SignatureHelpProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.SignatureHelpProvider + ) { } provideSignatureHelp(resource: URI, position: IPosition): TPromise { @@ -757,15 +723,11 @@ class SignatureHelpAdapter { class LinkProviderAdapter { - private _documents: ExtHostDocuments; - private _heapService: ExtHostHeapService; - private _provider: vscode.DocumentLinkProvider; - - constructor(documents: ExtHostDocuments, heapService: ExtHostHeapService, provider: vscode.DocumentLinkProvider) { - this._documents = documents; - this._heapService = heapService; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _heapService: ExtHostHeapService, + private readonly _provider: vscode.DocumentLinkProvider + ) { } provideLinks(resource: URI): TPromise { const doc = this._documents.getDocumentData(resource).document; diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index c34db952b6e..4c0370d20d8 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -14,7 +14,7 @@ import * as strings from 'vs/base/common/strings'; import URI, { UriComponents } from 'vs/base/common/uri'; import { PPromise, TPromise } from 'vs/base/common/winjs.base'; import { IItemAccessor, ScorerCache, compareItemsByScore, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; -import { ICachedSearchStats, IRawFileMatch2, IFolderQuery, IPatternInfo, IRawSearchQuery, ISearchQuery, ISearchCompleteStats, IFileMatch } from 'vs/platform/search/common/search'; +import { ICachedSearchStats, IFileMatch, IFolderQuery, IPatternInfo, IRawSearchQuery, ISearchQuery, ISearchCompleteStats, IRawFileMatch2 } from 'vs/platform/search/common/search'; import * as vscode from 'vscode'; import { ExtHostSearchShape, IMainContext, MainContext, MainThreadSearchShape } from './extHost.protocol'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -133,9 +133,11 @@ function reviveFolderQuery(rawFolderQuery: IFolderQuery): IFolder class TextSearchResultsCollector { private _batchedCollector: BatchedCollector; + private _currentFolderIdx: number; + private _currentRelativePath: string; private _currentFileMatch: IRawFileMatch2; - constructor(private _onResult: (result: IRawFileMatch2[]) => void) { + constructor(private folderQueries: IFolderQuery[], private _onResult: (result: IRawFileMatch2[]) => void) { this._batchedCollector = new BatchedCollector(512, items => this.sendItems(items)); } @@ -143,17 +145,15 @@ class TextSearchResultsCollector { // Collects TextSearchResults into IInternalFileMatches and collates using BatchedCollector. // This is efficient for ripgrep which sends results back one file at a time. It wouldn't be efficient for other search // providers that send results in random order. We could do this step afterwards instead. - if (this._currentFileMatch && (this._currentFileMatch.resource.folderIdx !== folderIdx || this._currentFileMatch.resource.relativePath !== data.path)) { + if (this._currentFileMatch && (this._currentFolderIdx !== folderIdx || this._currentRelativePath !== data.path)) { this.pushToCollector(); this._currentFileMatch = null; } if (!this._currentFileMatch) { + const resource = URI.file(path.join(this.folderQueries[folderIdx].folder.fsPath, data.path)); this._currentFileMatch = { - resource: { - folderIdx, - relativePath: data.path - }, + resource, lineMatches: [] }; } @@ -349,7 +349,7 @@ class QueryGlobTester { } return this._parsedIncludeExpression ? - TPromise.as(this._parsedIncludeExpression(testPath, basename, siblingsFn)).then(result => !result) : + TPromise.as(this._parsedIncludeExpression(testPath, basename, siblingsFn)).then(result => !!result) : TPromise.wrap(true); }).then(included => { return included; @@ -393,7 +393,7 @@ class TextSearchEngine { const folderQueries = this.config.folderQueries; return new PPromise<{ limitHit: boolean }, IRawFileMatch2[]>((resolve, reject, _onResult) => { - this.collector = new TextSearchResultsCollector(_onResult); + this.collector = new TextSearchResultsCollector(this.config.folderQueries, _onResult); const onResult = (match: vscode.TextSearchResult, folderIdx: number) => { if (this.isCanceled) { diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 2416526dcab..d8285ea3893 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -903,6 +903,11 @@ export class Hierarchy { } } +export enum CodeActionTrigger { + Automatic = 1, + Manual = 2, +} + export class CodeAction { title: string; diff --git a/src/vs/workbench/common/actions.ts b/src/vs/workbench/common/actions.ts index e0f736fc938..a2533c808d4 100644 --- a/src/vs/workbench/common/actions.ts +++ b/src/vs/workbench/common/actions.ts @@ -95,10 +95,10 @@ Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionR }; } - private _triggerAndDisposeAction(instantitationService: IInstantiationService, lifecycleService: ILifecycleService, descriptor: SyncActionDescriptor, args: any): Thenable { + private _triggerAndDisposeAction(instantiationService: IInstantiationService, lifecycleService: ILifecycleService, descriptor: SyncActionDescriptor, args: any): Thenable { // run action when workbench is created return lifecycleService.when(LifecyclePhase.Running).then(() => { - const actionInstance = instantitationService.createInstance(descriptor.syncDescriptor); + const actionInstance = instantiationService.createInstance(descriptor.syncDescriptor); try { actionInstance.label = descriptor.label || actionInstance.label; diff --git a/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts b/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts index 2d014c13990..ff59ed77adf 100644 --- a/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts +++ b/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts @@ -23,10 +23,11 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import URI from 'vs/base/common/uri'; import { join } from 'vs/base/common/paths'; import { IWindowsService } from 'vs/platform/windows/common/windows'; -import { IStorageService, } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, } from 'vs/platform/storage/common/storage'; import { TPromise } from 'vs/base/common/winjs.base'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { VIEWLET_ID as EXTENSIONS_VIEWLET_ID, IExtensionsViewlet } from 'vs/workbench/parts/extensions/common/extensions'; +import product from 'vs/platform/node/product'; // Register action to configure locale and related settings const registry = Registry.as(Extensions.WorkbenchActions); @@ -111,7 +112,57 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo }); } }); + return; } + + const bundledTranslations = (product['bundledTranslations'] || {})[platform.locale]; + if (language === platform.locale || !bundledTranslations || !bundledTranslations['languageName']) { + return; + } + + // The initial value for below dont get used. We just have it here so that they get localized. + // The localized strings get pulled into the "product.json" file during endgame to get shipped + let searchForLanguagePacks = localize('searchForLanguagePacks', "There are extensions in the Marketplace that can localize VS Code using the ${0} language.", bundledTranslations['languageName']); + let searchMarketplace = localize('searchMarketplace', "Search Marketplace"); + let dontShowAgain = localize('neverAgain', "Don't Show Again"); + + searchForLanguagePacks = bundledTranslations['searchForLanguagePacks']; + searchMarketplace = bundledTranslations['searchMarketplace']; + dontShowAgain = bundledTranslations['neverAgain']; + + const dontShowSearchLanguagePacksAgainKey = 'language.install.donotask'; + let dontShowSearchForLanguages = JSON.parse(this.storageService.get(dontShowSearchLanguagePacksAgainKey, StorageScope.GLOBAL, '[]')); + if (!Array.isArray(dontShowSearchForLanguages)) { + dontShowSearchForLanguages = []; + } + + if (dontShowSearchForLanguages.indexOf(platform.locale) > -1 + || !searchForLanguagePacks + || !searchMarketplace + || !dontShowAgain) { + return; + } + + this.notificationService.prompt(Severity.Info, searchForLanguagePacks, + [ + { + label: searchMarketplace, run: () => { + this.viewletService.openViewlet(EXTENSIONS_VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search(`tag:lp-${platform.locale}`); + viewlet.focus(); + }); + } + }, + { + label: dontShowAgain, run: () => { + dontShowSearchForLanguages.push(language); + this.storageService.store(dontShowSearchLanguagePacksAgainKey, StorageScope.GLOBAL, dontShowSearchForLanguages); + } + } + ]); + } private getLanguagePackExtension(language: string): TPromise { diff --git a/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css index 1bfec55e9f5..8c116ebdb82 100644 --- a/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css @@ -15,6 +15,32 @@ padding: 0px 10px 0px 0px; } +.settings-editor > .settings-header > .settings-preview-header { + margin-bottom: 5px; +} + +.settings-editor > .settings-header > .settings-preview-header .settings-preview-label { + opacity: .7; +} + +.settings-editor > .settings-header > .settings-preview-header .open-settings-button, +.settings-editor > .settings-header > .settings-preview-header .open-settings-button:hover, +.settings-editor > .settings-header > .settings-preview-header .open-settings-button:active { + padding: 0; + text-decoration: underline; + display: inline; +} + +.settings-editor > .settings-header > .settings-preview-header > .settings-preview-warning { + text-align: right; + text-transform: uppercase; + background: rgba(136, 136, 136, 0.3); + border-radius: 2px; + font-size: 0.8em; + padding: 0 3px; + margin-right: 7px; +} + .settings-editor > .settings-header > .search-container { position: relative; } @@ -54,13 +80,6 @@ display: flex; } -.settings-editor > .settings-header > .settings-header-controls .settings-header-controls-right .open-settings-button, -.settings-editor > .settings-header > .settings-header-controls .settings-header-controls-right .open-settings-button:hover, -.settings-editor > .settings-header > .settings-header-controls .settings-header-controls-right .open-settings-button:active { - padding: 0; - text-decoration: underline; -} - .settings-editor > .settings-header > .settings-header-controls .settings-header-controls-right #configured-only-checkbox { flex-shrink: 0; } @@ -92,16 +111,14 @@ white-space: normal; } -.settings-editor > .settings-body > .settings-list-container .monaco-list-row.odd { +.settings-editor > .settings-body > .settings-list-container .monaco-list-row.odd:not(.focused):not(.selected):not(:hover), +.settings-editor > .settings-body > .settings-list-container .monaco-list:not(:focus) .monaco-list-row.focused.odd:not(.selected):not(:hover), +.settings-editor > .settings-body > .settings-list-container .monaco-list:not(.focused) .monaco-list-row.focused.odd:not(.selected):not(:hover) { background-color: rgba(130, 130, 130, 0.04); } -.settings-editor > .settings-body > .settings-list-container .monaco-list-row:not(.odd) { - background-color: initial; -} - .settings-editor > .settings-body > .settings-list-container .monaco-list-row.setting-item { - padding: 3px 0px; + padding: 3px; } .settings-editor > .settings-body > .settings-list-container .monaco-list-row > .setting-item-container { @@ -162,6 +179,16 @@ cursor: pointer; } +.settings-editor > .settings-body > .settings-list-container .monaco-list-row.is-expandable:not(.is-expanded) .setting-item-description::after { + content: '…'; + display: block; + height: 4px; + width: 8px; + position: absolute; + bottom: 16px; + left: 590px; +} + .settings-editor > .settings-body > .settings-list-container .monaco-list-row .setting-item-value { display: flex; } @@ -250,6 +277,10 @@ margin-left: -15px; } +.settings-editor > .settings-body > .settings-list-container .settings-group-title-label { + margin: 5px 3px; +} + .settings-editor > .settings-body > .settings-list-container .settings-group-title-expanded, .settings-editor > .settings-body > .settings-list-container .settings-group-title-collapsed { cursor: pointer; @@ -283,5 +314,18 @@ position: absolute; content: ' '; left: -19px; - top: 14px; -} \ No newline at end of file + top: 4px; +} + +.settings-editor > .settings-body .settings-feedback-button { + color: rgb(255, 255, 255); + background-color: rgb(14, 99, 156); + position: absolute; + bottom: 0; + right: 0; + width: initial; + margin-right: 15px; + margin-bottom: 15px; + padding: 10px; + border-radius: 5px; +} diff --git a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts index 7baa5dec4e7..0f0eecbfa1c 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts @@ -36,8 +36,9 @@ import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbenc import { IPreferencesService, ISearchResult, ISetting, ISettingsEditorModel } from 'vs/workbench/services/preferences/common/preferences'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { DefaultSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; -import { IPreferencesSearchService, ISearchProvider } from '../common/preferences'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IPreferencesSearchService, ISearchProvider } from 'vs/workbench/parts/preferences/common/preferences'; +import { KeyCode } from 'vs/base/common/keyCodes'; const SETTINGS_ENTRY_TEMPLATE_ID = 'settings.entry.template'; const SETTINGS_GROUP_ENTRY_TEMPLATE_ID = 'settings.group.template'; @@ -59,6 +60,7 @@ interface ISettingItemEntry extends IListEntry { overriddenScopeList: string[]; isExpanded: boolean; isExpandable?: boolean; + isFocused?: boolean; type?: string | string[]; enum?: string[]; } @@ -105,8 +107,6 @@ export class SettingsEditor2 extends BaseEditor { private searchWidget: SearchWidget; private settingsTargetsWidget: SettingsTargetsWidget; - private showConfiguredSettingsOnly = false; - private showAllSettings = false; private showConfiguredSettingsOnlyCheckbox: HTMLInputElement; private settingsListContainer: HTMLElement; @@ -123,11 +123,16 @@ export class SettingsEditor2 extends BaseEditor { private currentLocalSearchProvider: ISearchProvider; private currentRemoteSearchProvider: ISearchProvider; - private searchResultModel: SearchResultModel; private pendingSettingModifiedReport: { key: string, value: any }; + // factor out tree/list viewmodel to somewhere outside this class + private searchResultModel: SearchResultModel; private groupExpanded = new Map(); private itemExpanded = new Map(); + private showConfiguredSettingsOnly = false; + private showAllSettings = false; + private inRender = false; + // constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -195,6 +200,24 @@ export class SettingsEditor2 extends BaseEditor { private createHeader(parent: HTMLElement): void { this.headerContainer = DOM.append(parent, $('.settings-header')); + const previewHeader = DOM.append(this.headerContainer, $('.settings-preview-header')); + + const previewAlert = DOM.append(previewHeader, $('span.settings-preview-warning')); + previewAlert.textContent = localize('previewWarning', "Preview"); + + const previewTextLabel = DOM.append(previewHeader, $('span.settings-preview-label')); + previewTextLabel.textContent = localize('previewLabel', "This is a preview of our new settings editor. You can also "); + const openSettingsButton = this._register(new Button(previewHeader, { title: true, buttonBackground: null, buttonHoverBackground: null })); + this._register(attachButtonStyler(openSettingsButton, this.themeService, { + buttonBackground: Color.transparent.toString(), + buttonHoverBackground: Color.transparent.toString(), + buttonForeground: 'foreground' + })); + openSettingsButton.label = localize('openSettingsLabel', "open the original editor."); + openSettingsButton.element.classList.add('open-settings-button'); + + this._register(openSettingsButton.onDidClick(() => this.openSettingsFile())); + const searchContainer = DOM.append(this.headerContainer, $('.search-container')); this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, searchContainer, { ariaLabel: localize('SearchSettings.AriaLabel', "Search settings"), @@ -202,6 +225,12 @@ export class SettingsEditor2 extends BaseEditor { focusKey: this.searchFocusContextKey })); this._register(this.searchWidget.onDidChange(() => this.onInputChanged())); + this._register(DOM.addStandardDisposableListener(this.searchWidget.domNode, 'keydown', e => { + if (e.keyCode === KeyCode.DownArrow) { + this.settingsList.focusFirst(); + this.settingsList.domFocus(); + } + })); const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls')); const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container')); @@ -222,17 +251,6 @@ export class SettingsEditor2 extends BaseEditor { showConfiguredSettingsOnlyLabel.htmlFor = 'configured-only-checkbox'; this._register(DOM.addDisposableListener(this.showConfiguredSettingsOnlyCheckbox, 'change', e => this.onShowConfiguredOnlyClicked())); - - const openSettingsButton = this._register(new Button(headerControlsContainerRight, { title: true, buttonBackground: null, buttonHoverBackground: null })); - this._register(attachButtonStyler(openSettingsButton, this.themeService, { - buttonBackground: Color.transparent.toString(), - buttonHoverBackground: Color.transparent.toString(), - buttonForeground: 'foreground' - })); - openSettingsButton.label = localize('openSettingsLabel', "Open settings.json"); - openSettingsButton.element.classList.add('open-settings-button'); - - this._register(openSettingsButton.onDidClick(() => this.openSettingsFile())); } private openSettingsFile(): TPromise { @@ -251,6 +269,7 @@ export class SettingsEditor2 extends BaseEditor { const bodyContainer = DOM.append(parent, $('.settings-body')); this.createList(bodyContainer); + this.createFeedbackButton(bodyContainer); } private createList(parent: HTMLElement): void { @@ -259,20 +278,13 @@ export class SettingsEditor2 extends BaseEditor { const settingItemRenderer = this.instantiationService.createInstance(SettingItemRenderer, this.settingsListContainer); this._register(settingItemRenderer.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value))); this._register(settingItemRenderer.onDidOpenSettings(() => this.openSettingsFile())); - this._register(settingItemRenderer.onDidToggleExpandSetting(e => { - this.itemExpanded.set(e.key, e.expandState === ExpandState.Expanded); - this.renderEntries(); - })); + this._register(settingItemRenderer.onDidToggleExpandSetting(e => this.toggleSettingExpanded(e))); - const buttonItemRenderer = new ButtonRowRenderer(); - this._register(buttonItemRenderer.onDidClick(e => this.onShowAllSettingsClicked())); + const buttonItemRenderer = this.instantiationService.createInstance(ButtonRowRenderer); + this._register(buttonItemRenderer.onDidClick(e => this.onShowAllSettingsToggled())); const groupTitleRenderer = new GroupTitleRenderer(); - this._register(groupTitleRenderer.onDidClickGroup(e => { - const isExpanded = !!this.groupExpanded.get(e); - this.groupExpanded.set(e, !isExpanded); - this.renderEntries(); - })); + this._register(groupTitleRenderer.onDidClickGroup(id => this.toggleGroupExpanded(id))); this.settingsList = this._register(this.instantiationService.createInstance( WorkbenchList, @@ -281,18 +293,65 @@ export class SettingsEditor2 extends BaseEditor { [settingItemRenderer, groupTitleRenderer, buttonItemRenderer], { identityProvider: e => e.id, - ariaLabel: localize('settingsListLabel', "Settings"), - focusOnMouseDown: false, - selectOnMouseDown: false, - keyboardSupport: false, - mouseSupport: false + ariaLabel: localize('settingsListLabel', "Settings") }) ) as WorkbenchList; - this.settingsList.style({ listHoverBackground: Color.transparent, listFocusOutline: Color.transparent }); + this._register(this.settingsList.onDidFocus(() => { + DOM.addClass(this.settingsList.getHTMLElement(), 'focused'); + })); + this._register(this.settingsList.onDidBlur(() => { + DOM.removeClass(this.settingsList.getHTMLElement(), 'focused'); + })); + + this.settingsList.onOpen(e => { + const entry = e.elements[0]; + if (!entry) { + return; + } + + if (entry.templateId === SETTINGS_GROUP_ENTRY_TEMPLATE_ID) { + this.toggleGroupExpanded(entry.id); + } else if (entry.templateId === SETTINGS_ENTRY_TEMPLATE_ID) { + this.toggleSettingExpanded(entry); + } else if (entry.templateId === BUTTON_ROW_ENTRY_TEMPLATE) { + this.onShowAllSettingsToggled(); + } + }); + + this._register(this.settingsList.onFocusChange(e => { + if (!this.inRender) { + this.renderEntries(); + } + })); } - private onShowAllSettingsClicked(): void { + private createFeedbackButton(parent: HTMLElement): void { + const feedbackButton = this._register(new Button(parent)); + feedbackButton.label = localize('feedbackButtonLabel', "Provide Feedback"); + feedbackButton.element.classList.add('settings-feedback-button'); + + this._register(attachButtonStyler(feedbackButton, this.themeService)); + this._register(feedbackButton.onDidClick(() => { + // Github master issue + window.open('https://go.microsoft.com/fwlink/?linkid=2000807'); + })); + } + + private toggleGroupExpanded(id: string): void { + const isExpanded = !!this.groupExpanded.get(id); + this.groupExpanded.set(id, !isExpanded); + this.renderEntries(); + } + + private toggleSettingExpanded(entry: ISettingItemEntry): void { + if (entry.isExpandable) { + this.itemExpanded.set(entry.key, !entry.isExpanded); + this.renderEntries(); + } + } + + private onShowAllSettingsToggled(): void { this.showAllSettings = !this.showAllSettings; this.render(CancellationToken.None); } @@ -509,6 +568,8 @@ export class SettingsEditor2 extends BaseEditor { const entries: ISettingItemEntry[] = []; const seenSettings = new Set(); + const focusedElement = this.settingsList.getFocusedElements()[0]; + const focusedId = focusedElement && focusedElement.id; for (let result of searchResults) { if (!result) { continue; @@ -516,7 +577,9 @@ export class SettingsEditor2 extends BaseEditor { for (let match of result.filterMatches) { if (!seenSettings.has(match.setting.key)) { - const entry = this.settingToEntry(match.setting); + const entry = this.settingToEntry(match.setting, 'search'); + entry.isFocused = entry.id === focusedId; + if (!this.showConfiguredSettingsOnly || entry.isConfigured) { seenSettings.add(entry.key); entries.push(entry); @@ -529,6 +592,7 @@ export class SettingsEditor2 extends BaseEditor { } private renderEntries(): void { + this.inRender = true; if (!this.defaultSettingsEditorModel) { return; } @@ -551,10 +615,15 @@ export class SettingsEditor2 extends BaseEditor { inputElementToFocus.focus(); } } + + this.inRender = false; } private getEntriesFromModel(): ListEntry[] { const entries: ListEntry[] = []; + const focusedElement = this.settingsList.getFocusedElements()[0]; + const focusedId = focusedElement && focusedElement.id; + for (let groupIdx = 0; groupIdx < this.defaultSettingsEditorModel.settingsGroups.length; groupIdx++) { if (groupIdx > 0 && !this.showAllSettings && !this.showConfiguredSettingsOnly) { break; @@ -567,7 +636,8 @@ export class SettingsEditor2 extends BaseEditor { if (isExpanded) { for (const section of group.sections) { for (const setting of section.settings) { - const entry = this.settingToEntry(setting); + const entry = this.settingToEntry(setting, group.id); + entry.isFocused = entry.id === focusedId; if (!this.showConfiguredSettingsOnly || entry.isConfigured) { groupEntries.push(entry); @@ -606,7 +676,7 @@ export class SettingsEditor2 extends BaseEditor { return entries; } - private settingToEntry(s: ISetting): ISettingItemEntry { + private settingToEntry(s: ISetting, groupId: string): ISettingItemEntry { const targetSelector = this.settingsTargetsWidget.settingsTarget === ConfigurationTarget.USER ? 'user' : 'workspace'; const inspected = this.configurationService.inspect(s.key); const isConfigured = typeof inspected[targetSelector] !== 'undefined'; @@ -623,7 +693,7 @@ export class SettingsEditor2 extends BaseEditor { const isExpanded = !!this.itemExpanded.get(s.key); return { - id: s.key, + id: `${groupId}_${s.key}`, key: s.key, value: displayValue, isConfigured, @@ -651,7 +721,7 @@ class SettingItemDelegate implements IDelegate { getHeight(entry: ListEntry) { if (entry.templateId === SETTINGS_GROUP_ENTRY_TEMPLATE_ID) { - return 42; + return 30; } if (entry.templateId === SETTINGS_ENTRY_TEMPLATE_ID) { @@ -701,6 +771,7 @@ interface ISettingItemTemplate extends IDisposableTemplate { categoryElement: HTMLElement; labelElement: HTMLElement; descriptionElement: HTMLElement; + showMoreElement: HTMLElement; valueElement: HTMLElement; overridesElement: HTMLElement; } @@ -723,11 +794,6 @@ interface ISettingChangeEvent { value: any; // undefined => reset unconfigure } -interface ISettingExpandEvent { - key: string; - expandState: ExpandState; -} - class SettingItemRenderer implements IRenderer { private readonly _onDidChangeSetting: Emitter = new Emitter(); @@ -736,8 +802,8 @@ class SettingItemRenderer implements IRenderer = new Emitter(); public readonly onDidOpenSettings: Event = this._onDidOpenSettings.event; - private readonly _onDidToggleExpandSetting: Emitter = new Emitter(); - public readonly onDidToggleExpandSetting: Event = this._onDidToggleExpandSetting.event; + private readonly _onDidToggleExpandSetting: Emitter = new Emitter(); + public readonly onDidToggleExpandSetting: Event = this._onDidToggleExpandSetting.event; get templateId(): string { return SETTINGS_ENTRY_TEMPLATE_ID; } @@ -763,6 +829,7 @@ class SettingItemRenderer implements IRenderer { const entry = template.context; if (entry && entry.isExpandable) { - const newState = entry.isExpanded ? ExpandState.Collapsed : ExpandState.Expanded; - that._onDidToggleExpandSetting.fire({ key: entry.key, expandState: newState }); + that._onDidToggleExpandSetting.fire(entry); } })); + + // Hack to prevent mousedown on value from causing list 'open' + toDispose.push(DOM.addDisposableListener(valueElement, 'mousedown', e => { + e.stopPropagation(); + })); } return template; @@ -825,6 +897,7 @@ class SettingItemRenderer implements IRendererDOM.append(template.valueElement, $('input.setting-value-checkbox.setting-value-input')); checkboxElement.type = 'checkbox'; checkboxElement.checked = entry.value; + checkboxElement.tabIndex = entry.isFocused ? 0 : -1; template.toDispose.push(DOM.addDisposableListener(checkboxElement, 'change', e => onChange(checkboxElement.checked))); } @@ -880,6 +954,9 @@ class SettingItemRenderer implements IRenderer onChange(entry.enum[e.index]))); @@ -890,6 +967,7 @@ class SettingItemRenderer implements IRenderer onChange(e))); @@ -900,6 +978,8 @@ class SettingItemRenderer implements IRenderer this._onDidOpenSettings.fire()); openSettingsButton.label = localize('editInSettingsJson', "Edit in settings.json"); openSettingsButton.element.classList.add('edit-in-settings-button'); + openSettingsButton.element.tabIndex = entry.isFocused ? 0 : -1; + template.toDispose.push(openSettingsButton); template.toDispose.push(attachButtonStyler(openSettingsButton, this.themeService, { buttonBackground: Color.transparent.toString(), @@ -975,6 +1055,8 @@ class ButtonRowRenderer implements IRenderer = new Emitter(); public readonly onDidClick: Event = this._onDidClick.event; + constructor(@IThemeService private themeService: IThemeService) { } + get templateId(): string { return BUTTON_ROW_ENTRY_TEMPLATE; } renderTemplate(parent: HTMLElement): IButtonRowTemplate { @@ -984,6 +1066,7 @@ class ButtonRowRenderer implements IRenderer(Extensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenRawDefaultSettingsAction, OpenRawDefaultSettingsAction.ID, OpenRawDefaultSettingsAction.LABEL), 'Preferences: Open Raw Default Settings', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenRawUserSettingsAction, OpenRawUserSettingsAction.ID, OpenRawUserSettingsAction.LABEL), 'Preferences: Open Raw User Settings', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettingsAction, OpenSettingsAction.ID, OpenSettingsAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.US_COMMA }), 'Preferences: Open Settings', category); -registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettings2Action, OpenSettings2Action.ID, OpenSettings2Action.LABEL), 'Preferences: Open Settings (Experimental)', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettingsAction, OpenSettingsAction.ID, OpenSettingsAction.LABEL), 'Preferences: Open Settings', category); +registry.registerWorkbenchAction(new SyncActionDescriptor(OpenSettings2Action, OpenSettings2Action.ID, OpenSettings2Action.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.US_COMMA }), 'Preferences: Open Settings (Experimental)', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalSettingsAction, OpenGlobalSettingsAction.ID, OpenGlobalSettingsAction.LABEL), 'Preferences: Open User Settings', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalKeybindingsAction, OpenGlobalKeybindingsAction.ID, OpenGlobalKeybindingsAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_S) }), 'Preferences: Open Keyboard Shortcuts', category); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenGlobalKeybindingsFileAction, OpenGlobalKeybindingsFileAction.ID, OpenGlobalKeybindingsFileAction.LABEL, { primary: null }), 'Preferences: Open Keyboard Shortcuts File', category); diff --git a/src/vs/workbench/parts/search/common/searchModel.ts b/src/vs/workbench/parts/search/common/searchModel.ts index 7d592089c62..186d45a23a6 100644 --- a/src/vs/workbench/parts/search/common/searchModel.ts +++ b/src/vs/workbench/parts/search/common/searchModel.ts @@ -791,9 +791,6 @@ export class SearchModel extends Disposable { private onSearchCompleted(completed: ISearchComplete, duration: number): ISearchComplete { this.currentRequest = null; - if (completed) { - this._searchResult.add(completed.results, false); - } const options: IPatternInfo = objects.assign({}, this._searchQuery.contentPattern); delete options.pattern; /* __GDPR__ diff --git a/src/vs/workbench/parts/search/test/common/searchModel.test.ts b/src/vs/workbench/parts/search/test/common/searchModel.test.ts index 673f4840f0f..91a808bc367 100644 --- a/src/vs/workbench/parts/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/parts/search/test/common/searchModel.test.ts @@ -77,12 +77,21 @@ suite('SearchModel', () => { }); }); - test('Search Model: Search adds to results', function () { + function ppromiseWithProgress(results: IFileMatch[]): () => PPromise { + return () => new PPromise((resolve, reject, progress) => { + process.nextTick(() => { + results.forEach(progress); + resolve(null); + }); + }); + } + + test('Search Model: Search adds to results', async () => { let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; - instantiationService.stub(ISearchService, 'search', PPromise.as({ results: results })); + instantiationService.stub(ISearchService, 'search', ppromiseWithProgress(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); - testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); + await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); let actual = testObject.searchResult.matches(); @@ -102,59 +111,27 @@ suite('SearchModel', () => { assert.ok(new Range(2, 1, 2, 2).equalsRange(actuaMatches[0].range())); }); - test('Search Model: Search adds to results during progress', function () { - let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; - let promise = new DeferredPPromise(); - instantiationService.stub(ISearchService, 'search', promise); - - let testObject = instantiationService.createInstance(SearchModel); - let result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); - - promise.progress(results[0]); - promise.progress(results[1]); - promise.complete({ results: [], stats: testSearchStats }); - - return result.then(() => { - let actual = testObject.searchResult.matches(); - - assert.equal(2, actual.length); - assert.equal('file://c:/1', actual[0].resource().toString()); - - let actuaMatches = actual[0].matches(); - assert.equal(2, actuaMatches.length); - assert.equal('preview 1', actuaMatches[0].text()); - assert.ok(new Range(2, 2, 2, 5).equalsRange(actuaMatches[0].range())); - assert.equal('preview 1', actuaMatches[1].text()); - assert.ok(new Range(2, 5, 2, 12).equalsRange(actuaMatches[1].range())); - - actuaMatches = actual[1].matches(); - assert.equal(1, actuaMatches.length); - assert.equal('preview 2', actuaMatches[0].text()); - assert.ok(new Range(2, 1, 2, 2).equalsRange(actuaMatches[0].range())); - }); - }); - - test('Search Model: Search reports telemetry on search completed', function () { + test('Search Model: Search reports telemetry on search completed', async () => { let target = instantiationService.spy(ITelemetryService, 'publicLog'); let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; - instantiationService.stub(ISearchService, 'search', PPromise.as({ results: results })); + instantiationService.stub(ISearchService, 'search', ppromiseWithProgress(results)); let testObject = instantiationService.createInstance(SearchModel); - testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); + await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); - assert.ok(target.calledOnce); + assert.ok(target.calledThrice); const data = target.args[0]; data[1].duration = -1; - assert.deepEqual(['searchResultsShown', { count: 3, fileCount: 2, options: {}, duration: -1, useRipgrep: undefined }], data); + assert.deepEqual(['searchResultsFirstRender', { duration: -1 }], data); }); - test('Search Model: Search reports timed telemetry on search when progress is not called', function () { + test('Search Model: Search reports timed telemetry on search when progress is not called', () => { let target2 = sinon.spy(); stub(nullEvent, 'stop', target2); let target1 = sinon.stub().returns(nullEvent); instantiationService.stub(ITelemetryService, 'publicLog', target1); - instantiationService.stub(ISearchService, 'search', PPromise.as({ results: [] })); + instantiationService.stub(ISearchService, 'search', ppromiseWithProgress([])); let testObject = instantiationService.createInstance(SearchModel); const result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); @@ -168,7 +145,7 @@ suite('SearchModel', () => { }); }); - test('Search Model: Search reports timed telemetry on search when progress is called', function () { + test('Search Model: Search reports timed telemetry on search when progress is called', () => { let target2 = sinon.spy(); stub(nullEvent, 'stop', target2); let target1 = sinon.stub().returns(nullEvent); @@ -192,7 +169,7 @@ suite('SearchModel', () => { }); }); - test('Search Model: Search reports timed telemetry on search when error is called', function () { + test('Search Model: Search reports timed telemetry on search when error is called', () => { let target2 = sinon.spy(); stub(nullEvent, 'stop', target2); let target1 = sinon.stub().returns(nullEvent); @@ -215,7 +192,7 @@ suite('SearchModel', () => { }); }); - test('Search Model: Search reports timed telemetry on search when error is cancelled error', function () { + test('Search Model: Search reports timed telemetry on search when error is cancelled error', () => { let target2 = sinon.spy(); stub(nullEvent, 'stop', target2); let target1 = sinon.stub().returns(nullEvent); @@ -238,11 +215,11 @@ suite('SearchModel', () => { }); }); - test('Search Model: Search results are cleared during search', function () { + test('Search Model: Search results are cleared during search', async () => { let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; - instantiationService.stub(ISearchService, 'search', PPromise.as({ results: results })); + instantiationService.stub(ISearchService, 'search', ppromiseWithProgress(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); - testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); + await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); assert.ok(!testObject.searchResult.isEmpty()); instantiationService.stub(ISearchService, 'search', new DeferredPPromise()); @@ -251,7 +228,7 @@ suite('SearchModel', () => { assert.ok(testObject.searchResult.isEmpty()); }); - test('Search Model: Previous search is cancelled when new search is called', function () { + test('Search Model: Previous search is cancelled when new search is called', async () => { let target = sinon.spy(); instantiationService.stub(ISearchService, 'search', new DeferredPPromise((c, e, p) => { }, target)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); @@ -263,29 +240,29 @@ suite('SearchModel', () => { assert.ok(target.calledOnce); }); - test('getReplaceString returns proper replace string for regExpressions', function () { + test('getReplaceString returns proper replace string for regExpressions', async () => { let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]]))]; - instantiationService.stub(ISearchService, 'search', PPromise.as({ results: results })); + instantiationService.stub(ISearchService, 'search', ppromiseWithProgress(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); - testObject.search({ contentPattern: { pattern: 're' }, type: 1, folderQueries }); + await testObject.search({ contentPattern: { pattern: 're' }, type: 1, folderQueries }); testObject.replaceString = 'hello'; let match = testObject.searchResult.matches()[0].matches()[0]; assert.equal('hello', match.replaceString); - testObject.search({ contentPattern: { pattern: 're', isRegExp: true }, type: 1, folderQueries }); + await testObject.search({ contentPattern: { pattern: 're', isRegExp: true }, type: 1, folderQueries }); match = testObject.searchResult.matches()[0].matches()[0]; assert.equal('hello', match.replaceString); - testObject.search({ contentPattern: { pattern: 're(?:vi)', isRegExp: true }, type: 1, folderQueries }); + await testObject.search({ contentPattern: { pattern: 're(?:vi)', isRegExp: true }, type: 1, folderQueries }); match = testObject.searchResult.matches()[0].matches()[0]; assert.equal('hello', match.replaceString); - testObject.search({ contentPattern: { pattern: 'r(e)(?:vi)', isRegExp: true }, type: 1, folderQueries }); + await testObject.search({ contentPattern: { pattern: 'r(e)(?:vi)', isRegExp: true }, type: 1, folderQueries }); match = testObject.searchResult.matches()[0].matches()[0]; assert.equal('hello', match.replaceString); - testObject.search({ contentPattern: { pattern: 'r(e)(?:vi)', isRegExp: true }, type: 1, folderQueries }); + await testObject.search({ contentPattern: { pattern: 'r(e)(?:vi)', isRegExp: true }, type: 1, folderQueries }); testObject.replaceString = 'hello$1'; match = testObject.searchResult.matches()[0].matches()[0]; assert.equal('helloe', match.replaceString); diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index ff748df861b..4ca2847cbf8 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -494,6 +494,8 @@ export interface ITerminalCommandTracker { scrollToNextCommand(): void; selectToPreviousCommand(): void; selectToNextCommand(): void; + selectToPreviousLine(): void; + selectToNextLine(): void; } export interface ITerminalProcessManager extends IDisposable { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts index 2cbcf6f69d0..c1b8fd886ba 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -18,7 +18,7 @@ import { getTerminalDefaultShellUnixLike, getTerminalDefaultShellWindows } from import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KillTerminalAction, ClearSelectionTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, CreateNewInActiveWorkspaceTerminalAction, FocusActiveTerminalAction, FocusNextTerminalAction, FocusPreviousTerminalAction, SelectDefaultShellWindowsTerminalAction, RunSelectedTextInTerminalAction, RunActiveFileInTerminalAction, ScrollDownTerminalAction, ScrollDownPageTerminalAction, ScrollToBottomTerminalAction, ScrollUpTerminalAction, ScrollUpPageTerminalAction, ScrollToTopTerminalAction, TerminalPasteAction, ToggleTerminalAction, ClearTerminalAction, AllowWorkspaceShellTerminalCommand, DisallowWorkspaceShellTerminalCommand, RenameTerminalAction, SelectAllTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, ShowNextFindTermTerminalFindWidgetAction, ShowPreviousFindTermTerminalFindWidgetAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, TERMINAL_PICKER_PREFIX, MoveToLineStartTerminalAction, MoveToLineEndTerminalAction, SplitTerminalAction, SplitInActiveWorkspaceTerminalAction, FocusPreviousPaneTerminalAction, FocusNextPaneTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, ResizePaneDownTerminalAction, ScrollToPreviousCommandAction, ScrollToNextCommandAction, SelectToPreviousCommandAction, SelectToNextCommandAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; +import { KillTerminalAction, ClearSelectionTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, CreateNewInActiveWorkspaceTerminalAction, FocusActiveTerminalAction, FocusNextTerminalAction, FocusPreviousTerminalAction, SelectDefaultShellWindowsTerminalAction, RunSelectedTextInTerminalAction, RunActiveFileInTerminalAction, ScrollDownTerminalAction, ScrollDownPageTerminalAction, ScrollToBottomTerminalAction, ScrollUpTerminalAction, ScrollUpPageTerminalAction, ScrollToTopTerminalAction, TerminalPasteAction, ToggleTerminalAction, ClearTerminalAction, AllowWorkspaceShellTerminalCommand, DisallowWorkspaceShellTerminalCommand, RenameTerminalAction, SelectAllTerminalAction, FocusTerminalFindWidgetAction, HideTerminalFindWidgetAction, ShowNextFindTermTerminalFindWidgetAction, ShowPreviousFindTermTerminalFindWidgetAction, DeleteWordLeftTerminalAction, DeleteWordRightTerminalAction, QuickOpenActionTermContributor, QuickOpenTermAction, TERMINAL_PICKER_PREFIX, MoveToLineStartTerminalAction, MoveToLineEndTerminalAction, SplitTerminalAction, SplitInActiveWorkspaceTerminalAction, FocusPreviousPaneTerminalAction, FocusNextPaneTerminalAction, ResizePaneLeftTerminalAction, ResizePaneRightTerminalAction, ResizePaneUpTerminalAction, ResizePaneDownTerminalAction, ScrollToPreviousCommandAction, ScrollToNextCommandAction, SelectToPreviousCommandAction, SelectToNextCommandAction, SelectToPreviousLineAction, SelectToNextLineAction } from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; import { Registry } from 'vs/platform/registry/common/platform'; import { ShowAllCommandsAction } from 'vs/workbench/parts/quickopen/browser/commandsHandler'; import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; @@ -304,7 +304,9 @@ configurationRegistry.registerConfiguration({ ScrollToPreviousCommandAction.ID, ScrollToNextCommandAction.ID, SelectToPreviousCommandAction.ID, - SelectToNextCommandAction.ID + SelectToNextCommandAction.ID, + SelectToPreviousLineAction.ID, + SelectToNextLineAction.ID ].sort() }, 'terminal.integrated.env.osx': { @@ -520,6 +522,8 @@ actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToNextComm primary: null, mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow } }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select To Next Command', category); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToPreviousLineAction, SelectToPreviousLineAction.ID, SelectToPreviousLineAction.LABEL), 'Terminal: Select To Previous Line', category); +actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(SelectToNextLineAction, SelectToNextLineAction.ID, SelectToNextLineAction.LABEL), 'Terminal: Select To Next Line', category); terminalCommands.setup(); diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts index 26ae132f0cf..66c7c67b786 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts @@ -1130,3 +1130,45 @@ export class SelectToNextCommandAction extends Action { return TPromise.as(void 0); } } + +export class SelectToPreviousLineAction extends Action { + public static readonly ID = 'workbench.action.terminal.selectToPreviousLine'; + public static readonly LABEL = nls.localize('workbench.action.terminal.selectToPreviousLine', "Select To Previous Line"); + + constructor( + id: string, label: string, + @ITerminalService private terminalService: ITerminalService + ) { + super(id, label); + } + + public run(): TPromise { + const instance = this.terminalService.getActiveInstance(); + if (instance) { + instance.commandTracker.selectToPreviousLine(); + instance.focus(); + } + return TPromise.as(void 0); + } +} + +export class SelectToNextLineAction extends Action { + public static readonly ID = 'workbench.action.terminal.selectToNextLine'; + public static readonly LABEL = nls.localize('workbench.action.terminal.selectToNextLine', "Select To Next Line"); + + constructor( + id: string, label: string, + @ITerminalService private terminalService: ITerminalService + ) { + super(id, label); + } + + public run(): TPromise { + const instance = this.terminalService.getActiveInstance(); + if (instance) { + instance.commandTracker.selectToNextLine(); + instance.focus(); + } + return TPromise.as(void 0); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts b/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts index 7229460ec01..d11af07bd79 100644 --- a/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts +++ b/src/vs/workbench/parts/terminal/node/terminalCommandTracker.ts @@ -24,6 +24,7 @@ export enum ScrollPosition { export class TerminalCommandTracker implements ITerminalCommandTracker { private _currentMarker: IMarker | Boundary = Boundary.Bottom; private _selectionStart: IMarker | Boundary | null = null; + private _isDisposable: boolean = false; constructor( private _xterm: Terminal @@ -58,6 +59,10 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { markerIndex = this._xterm.markers.length - 1; } else if (this._currentMarker === Boundary.Top) { markerIndex = -1; + } else if (this._isDisposable) { + markerIndex = this._findPreviousCommand(); + this._currentMarker.dispose(); + this._isDisposable = false; } else { markerIndex = this._xterm.markers.indexOf(this._currentMarker) - 1; } @@ -82,6 +87,10 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { markerIndex = this._xterm.markers.length; } else if (this._currentMarker === Boundary.Top) { markerIndex = 0; + } else if (this._isDisposable) { + markerIndex = this._findNextCommand(); + this._currentMarker.dispose(); + this._isDisposable = false; } else { markerIndex = this._xterm.markers.indexOf(this._currentMarker) + 1; } @@ -120,6 +129,24 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { this._selectLines(this._currentMarker, this._selectionStart); } + public selectToPreviousLine(): void { + if (this._selectionStart === null) { + this._selectionStart = this._currentMarker; + } + + this.scrollToPreviousLine(ScrollPosition.Middle, true); + this._selectLines(this._currentMarker, this._selectionStart); + } + + public selectToNextLine(): void { + if (this._selectionStart === null) { + this._selectionStart = this._currentMarker; + } + + this.scrollToNextLine(ScrollPosition.Middle, true); + this._selectLines(this._currentMarker, this._selectionStart); + } + private _selectLines(start: IMarker | Boundary, end: IMarker | Boundary | null): void { if (end === null) { end = Boundary.Bottom; @@ -153,4 +180,96 @@ export class TerminalCommandTracker implements ITerminalCommandTracker { return marker.line; } + + public scrollToPreviousLine(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void { + if (!retainSelection) { + this._selectionStart = null; + } + + if (this._currentMarker === Boundary.Top) { + this._xterm.scrollToTop(); + return; + } + + if (this._currentMarker === Boundary.Bottom) { + this._currentMarker = this._xterm.addMarker(this._getOffset() - 1); + } else { + let offset = this._getOffset(); + if (this._isDisposable) { + this._currentMarker.dispose(); + } + this._currentMarker = this._xterm.addMarker(offset - 1); + } + this._isDisposable = true; + this._scrollToMarker(this._currentMarker, scrollPosition); + } + + public scrollToNextLine(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void { + if (!retainSelection) { + this._selectionStart = null; + } + + if (this._currentMarker === Boundary.Bottom) { + this._xterm.scrollToBottom(); + return; + } + + if (this._currentMarker === Boundary.Top) { + this._currentMarker = this._xterm.addMarker(this._getOffset() + 1); + } else { + let offset = this._getOffset(); + if (this._isDisposable) { + this._currentMarker.dispose(); + } + this._currentMarker = this._xterm.addMarker(offset + 1); + } + this._isDisposable = true; + this._scrollToMarker(this._currentMarker, scrollPosition); + } + + private _getOffset(): number { + if (this._currentMarker === Boundary.Bottom) { + return 0; + } else if (this._currentMarker === Boundary.Top) { + return 0 - (this._xterm.buffer.ybase + this._xterm.buffer.y); + } else { + let offset = this._getLine(this._currentMarker); + offset -= this._xterm.buffer.ybase + this._xterm.buffer.y; + return offset; + } + } + + private _findPreviousCommand(): number { + if (this._currentMarker === Boundary.Top) { + return 0; + } else if (this._currentMarker === Boundary.Bottom) { + return this._xterm.markers.length - 1; + } + + let i; + for (i = this._xterm.markers.length - 1; i >= 0; i--) { + if (this._xterm.markers[i].line < this._currentMarker.line) { + return i; + } + } + + return -1; + } + + private _findNextCommand(): number { + if (this._currentMarker === Boundary.Top) { + return 0; + } else if (this._currentMarker === Boundary.Bottom) { + return this._xterm.markers.length - 1; + } + + let i; + for (i = 0; i < this._xterm.markers.length; i++) { + if (this._xterm.markers[i].line > this._currentMarker.line) { + return i; + } + } + + return this._xterm.markers.length; + } } diff --git a/src/vs/workbench/parts/terminal/test/node/terminalCommandTracker.test.ts b/src/vs/workbench/parts/terminal/test/node/terminalCommandTracker.test.ts index e56e7f4f6aa..003f76b383b 100644 --- a/src/vs/workbench/parts/terminal/test/node/terminalCommandTracker.test.ts +++ b/src/vs/workbench/parts/terminal/test/node/terminalCommandTracker.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { Terminal } from 'vscode-xterm'; import { TerminalCommandTracker } from 'vs/workbench/parts/terminal/node/terminalCommandTracker'; +import { isWindows } from 'vs/base/common/platform'; interface TestTerminal extends Terminal { writeBuffer: string[]; @@ -80,35 +81,70 @@ suite('Workbench - TerminalCommandTracker', () => { commandTracker.scrollToNextCommand(); assert.equal(xterm.buffer.ydisp, 20); }); - // test('should select to the next and previous commands', () => { - // (window).matchMedia = () => { - // return { addListener: () => {} } - // }; - // xterm.open(document.createElement('div')); + test('should select to the next and previous commands', () => { + (window).matchMedia = () => { + return { addListener: () => { } }; + }; + xterm.open(document.createElement('div')); - // syncWrite(xterm, '\r0'); - // syncWrite(xterm, '\n\r1'); - // syncWrite(xterm, '\x1b[3G'); // Move cursor to column 3 - // xterm.emit('key', '\x0d'); // Mark line - // assert.equal(xterm.markers[0].line, 10); - // syncWrite(xterm, '\n\r2'); - // syncWrite(xterm, '\x1b[3G'); // Move cursor to column 3 - // xterm.emit('key', '\x0d'); // Mark line - // assert.equal(xterm.markers[1].line, 11); - // syncWrite(xterm, '\n\r3'); + syncWrite(xterm, '\r0'); + syncWrite(xterm, '\n\r1'); + syncWrite(xterm, '\x1b[3G'); // Move cursor to column 3 + xterm.emit('key', '\x0d'); // Mark line + assert.equal(xterm.markers[0].line, 10); + syncWrite(xterm, '\n\r2'); + syncWrite(xterm, '\x1b[3G'); // Move cursor to column 3 + xterm.emit('key', '\x0d'); // Mark line + assert.equal(xterm.markers[1].line, 11); + syncWrite(xterm, '\n\r3'); - // assert.equal(xterm.buffer.ybase, 3); - // assert.equal(xterm.buffer.ydisp, 3); + assert.equal(xterm.buffer.ybase, 3); + assert.equal(xterm.buffer.ydisp, 3); - // assert.equal(xterm.getSelection(), ''); - // commandTracker.selectToPreviousCommand(); - // assert.equal(xterm.getSelection(), '2'); - // commandTracker.selectToPreviousCommand(); - // assert.equal(xterm.getSelection(), '1\n2'); - // commandTracker.selectToNextCommand(); - // assert.equal(xterm.getSelection(), '2'); - // commandTracker.selectToNextCommand(); - // assert.equal(xterm.getSelection(), '\n'); - // }); + assert.equal(xterm.getSelection(), ''); + commandTracker.selectToPreviousCommand(); + assert.equal(xterm.getSelection(), '2'); + commandTracker.selectToPreviousCommand(); + assert.equal(xterm.getSelection(), isWindows ? '1\r\n2' : '1\n2'); + commandTracker.selectToNextCommand(); + assert.equal(xterm.getSelection(), '2'); + commandTracker.selectToNextCommand(); + assert.equal(xterm.getSelection(), isWindows ? '\r\n' : '\n'); + }); + test('should select to the next and previous lines & commands', () => { + (window).matchMedia = () => { + return { addListener: () => { } }; + }; + xterm.open(document.createElement('div')); + + syncWrite(xterm, '\r0'); + syncWrite(xterm, '\n\r1'); + syncWrite(xterm, '\x1b[3G'); // Move cursor to column 3 + xterm.emit('key', '\x0d'); // Mark line + assert.equal(xterm.markers[0].line, 10); + syncWrite(xterm, '\n\r2'); + syncWrite(xterm, '\x1b[3G'); // Move cursor to column 3 + xterm.emit('key', '\x0d'); // Mark line + assert.equal(xterm.markers[1].line, 11); + syncWrite(xterm, '\n\r3'); + + assert.equal(xterm.buffer.ybase, 3); + assert.equal(xterm.buffer.ydisp, 3); + + assert.equal(xterm.getSelection(), ''); + commandTracker.selectToPreviousLine(); + assert.equal(xterm.getSelection(), '2'); + commandTracker.selectToNextLine(); + commandTracker.selectToNextLine(); + assert.equal(xterm.getSelection(), '3'); + commandTracker.selectToPreviousCommand(); + commandTracker.selectToPreviousCommand(); + commandTracker.selectToNextLine(); + assert.equal(xterm.getSelection(), '2'); + commandTracker.selectToPreviousCommand(); + assert.equal(xterm.getSelection(), isWindows ? '1\r\n2' : '1\n2'); + commandTracker.selectToPreviousLine(); + assert.equal(xterm.getSelection(), isWindows ? '0\r\n1\r\n2' : '0\n1\n2'); + }); }); }); \ No newline at end of file diff --git a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts index 2c6675b4dae..2bc337d218f 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -9,7 +9,7 @@ import * as path from 'path'; import * as extfs from 'vs/base/node/extfs'; import URI, { UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IRawFileMatch2, IRawSearchQuery, QueryType, ISearchQuery, IPatternInfo } from 'vs/platform/search/common/search'; +import { IRawFileMatch2, IRawSearchQuery, QueryType, ISearchQuery, IPatternInfo, IFileMatch } from 'vs/platform/search/common/search'; import { MainContext, MainThreadSearchShape } from 'vs/workbench/api/node/extHost.protocol'; import { ExtHostSearch } from 'vs/workbench/api/node/extHostSearch'; import { TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; @@ -78,7 +78,7 @@ suite('ExtHostSearch', () => { return (mockMainThreadSearch.results).map(r => URI.revive(r)); } - async function runTextSearch(pattern: IPatternInfo, query: IRawSearchQuery, cancel = false): TPromise { + async function runTextSearch(pattern: IPatternInfo, query: IRawSearchQuery, cancel = false): TPromise { try { const p = extHostSearch.$provideTextSearchResults(mockMainThreadSearch.lastHandle, 0, pattern, query); if (cancel) { @@ -95,7 +95,12 @@ suite('ExtHostSearch', () => { } await rpcProtocol.sync(); - return mockMainThreadSearch.results; + return (mockMainThreadSearch.results).map(r => ({ + ...r, + ...{ + resource: URI.revive(r.resource) + } + })); } setup(() => { @@ -114,7 +119,7 @@ suite('ExtHostSearch', () => { return rpcProtocol.sync(); }); - const rootFolderA = URI.file('/foo/bar'); + const rootFolderA = URI.file('/foo/bar1'); const rootFolderB = URI.file('/foo/bar2'); // const rootFolderC = URI.file('/foo/bar3'); @@ -624,6 +629,7 @@ suite('ExtHostSearch', () => { } function makeTextResult(relativePath: string): vscode.TextSearchResult { + relativePath = relativePath.replace(/\//g, path.sep); return { preview: makePreview('foo'), range: new Range(0, 0, 0, 3), @@ -647,15 +653,17 @@ suite('ExtHostSearch', () => { }; } - function assertResults(actual: IRawFileMatch2[], expected: vscode.TextSearchResult[]) { + function assertResults(actual: IFileMatch[], expected: vscode.TextSearchResult[]) { const actualTextSearchResults: vscode.TextSearchResult[] = []; for (let fileMatch of actual) { + // Make relative + const relativePath = fileMatch.resource.fsPath.substr(rootFolderA.fsPath.length + 1); for (let lineMatch of fileMatch.lineMatches) { for (let [offset, length] of lineMatch.offsetAndLengths) { actualTextSearchResults.push({ preview: { text: lineMatch.preview, match: null }, range: new Range(lineMatch.lineNumber, offset, lineMatch.lineNumber, length + offset), - path: fileMatch.resource.relativePath + path: relativePath }); } } @@ -951,6 +959,35 @@ suite('ExtHostSearch', () => { makeTextResult('file3.js')]); }); + test('include pattern applied', async () => { + const providedResults: vscode.TextSearchResult[] = [ + makeTextResult('file1.js'), + makeTextResult('file1.ts') + ]; + + await registerTestSearchProvider({ + provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + providedResults.forEach(r => progress.report(r)); + return TPromise.wrap(null); + } + }); + + const query: ISearchQuery = { + type: QueryType.Text, + + includePattern: { + '*.ts': true + }, + + folderQueries: [ + { folder: rootFolderA } + ] + }; + + const results = await runTextSearch(getPattern('foo'), query); + assertResults(results, providedResults.slice(1)); + }); + test('max results = 1', async () => { const providedResults: vscode.TextSearchResult[] = [ makeTextResult('file1.ts'), diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadDocumentsAndEditors.test.ts index b17ff44454c..2392b091e98 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadDocumentsAndEditors.test.ts @@ -20,6 +20,7 @@ import { Event } from 'vs/base/common/event'; import { ITextModel } from 'vs/editor/common/model'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IFileService } from 'vs/platform/files/common/files'; suite('MainThreadDocumentsAndEditors', () => { @@ -55,6 +56,10 @@ suite('MainThreadDocumentsAndEditors', () => { const workbenchEditorService = new TestEditorService(); const editorGroupService = new TestEditorGroupsService(); + const fileService = new class extends mock() { + onAfterOperation = Event.None; + }; + /* tslint:disable */ new MainThreadDocumentsAndEditors( SingleProxyRPCProtocol(new class extends mock() { @@ -65,7 +70,7 @@ suite('MainThreadDocumentsAndEditors', () => { workbenchEditorService, codeEditorService, null, - null, + fileService, null, null, editorGroupService, diff --git a/test/smoke/src/areas/css/css.test.ts b/test/smoke/src/areas/css/css.test.ts index 33dc79bc1c3..fa27427919f 100644 --- a/test/smoke/src/areas/css/css.test.ts +++ b/test/smoke/src/areas/css/css.test.ts @@ -28,7 +28,7 @@ export function setup() { await app.workbench.problems.hideProblemsView(); }); - it('verifies that warning becomes an error once setting changed', async function () { + it.skip('verifies that warning becomes an error once setting changed', async function () { // settings might take a while to update? this.timeout(40000); diff --git a/test/smoke/src/areas/preferences/preferences.test.ts b/test/smoke/src/areas/preferences/preferences.test.ts index 1b2bbbec548..b5de701993f 100644 --- a/test/smoke/src/areas/preferences/preferences.test.ts +++ b/test/smoke/src/areas/preferences/preferences.test.ts @@ -7,7 +7,7 @@ import { Application } from '../../application'; import { ActivityBarPosition } from '../activitybar/activityBar'; export function setup() { - describe('Preferences', () => { + describe.skip('Preferences', () => { it('turns off editor line numbers and verifies the live change', async function () { const app = this.app as Application; diff --git a/yarn.lock b/yarn.lock index c960fa4db67..a265683c55f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5997,9 +5997,9 @@ vscode-textmate@^3.3.3: fast-plist "^0.1.2" oniguruma "^6.0.1" -vscode-xterm@3.5.0-beta6: - version "3.5.0-beta6" - resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.5.0-beta6.tgz#215df0c812536830ce65c3266ad5fc9ffe7b4a4f" +vscode-xterm@3.5.0-beta8: + version "3.5.0-beta8" + resolved "https://registry.yarnpkg.com/vscode-xterm/-/vscode-xterm-3.5.0-beta8.tgz#beaf158e00ce8341caa18703b3d1ed18be78b676" vso-node-api@^6.1.2-preview: version "6.1.2-preview"