diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 963da6581ec174b954726d270803bbb2b879510a..daf7bac0107b5c3e89fdefaabe47d079d220ed19 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -23,7 +23,7 @@ "onCommand:markdown.showLockedPreviewToSide", "onCommand:markdown.showSource", "onCommand:markdown.showPreviewSecuritySelector", - "onView:markdown.preview" + "onWebviewPanel:markdown.preview" ], "contributes": { "commands": [ diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 5312e4109bb5aecb16b1c604f871d397c28cd801..a9d95e36ce87e3789a5cf362db95bdb6bd771541 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -17,6 +17,7 @@ import { MarkdownEngine } from './markdownEngine'; import { getMarkdownExtensionContributions } from './markdownExtensions'; import { ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './security'; import { loadDefaultTelemetryReporter } from './telemetryReporter'; +import { githubSlugifier } from './slugify'; export function activate(context: vscode.ExtensionContext) { @@ -26,7 +27,7 @@ export function activate(context: vscode.ExtensionContext) { const contributions = getMarkdownExtensionContributions(); const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState, context.workspaceState); - const engine = new MarkdownEngine(contributions); + const engine = new MarkdownEngine(contributions, githubSlugifier); const logger = new Logger(); const selector: vscode.DocumentSelector = [ diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 6d78b575dab0990c2c4207b9fbd8a03824d4140c..7522eb8b782dca8008b525c879cdb68124c458c3 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -7,7 +7,7 @@ import { MarkdownIt, Token } from 'markdown-it'; import * as path from 'path'; import * as vscode from 'vscode'; import { MarkdownContributions } from './markdownExtensions'; -import { stripSlugifier } from './slugify'; +import { Slugifier } from './slugify'; const FrontMatterRegex = /^---\s*[^]*?(-{3}|\.{3})\s*/; @@ -19,7 +19,8 @@ export class MarkdownEngine { private currentDocument?: vscode.Uri; public constructor( - private readonly extensionPreviewResourceProvider: MarkdownContributions + private readonly extensionPreviewResourceProvider: MarkdownContributions, + private readonly slugifier: Slugifier, ) { } private usePlugin(factory: (md: any) => any): void { @@ -49,7 +50,7 @@ export class MarkdownEngine { return `
${this.md!.utils.escapeHtml(str)}
`; } }).use(mdnh, { - slugify: (header: string) => stripSlugifier.fromHeading(header).value + slugify: (header: string) => this.slugifier.fromHeading(header).value }); for (const plugin of this.extensionPreviewResourceProvider.markdownItPlugins) { @@ -145,13 +146,13 @@ export class MarkdownEngine { if (fragment) { uri = uri.with({ - fragment: stripSlugifier.fromHeading(fragment).value + fragment: this.slugifier.fromHeading(fragment).value }); } return normalizeLink(uri.with({ scheme: 'vscode-resource' }).toString(true)); } else if (!uri.scheme && !uri.path && uri.fragment) { return normalizeLink(uri.with({ - fragment: stripSlugifier.fromHeading(uri.fragment).value + fragment: this.slugifier.fromHeading(uri.fragment).value }).toString(true)); } } catch (e) { diff --git a/extensions/markdown-language-features/src/slugify.ts b/extensions/markdown-language-features/src/slugify.ts index 768a0c64d9751dd4299299c9403780cc4ebdaa28..c3e167e89e07d2c61b09f7ca23818a14b96a4d25 100644 --- a/extensions/markdown-language-features/src/slugify.ts +++ b/extensions/markdown-language-features/src/slugify.ts @@ -17,19 +17,16 @@ export interface Slugifier { fromHeading(heading: string): Slug; } -export const stripSlugifier: Slugifier = new class implements Slugifier { - private readonly specialChars: any = { 'à': 'a', 'ä': 'a', 'ã': 'a', 'á': 'a', 'â': 'a', 'æ': 'a', 'å': 'a', 'ë': 'e', 'è': 'e', 'é': 'e', 'ê': 'e', 'î': 'i', 'ï': 'i', 'ì': 'i', 'í': 'i', 'ò': 'o', 'ó': 'o', 'ö': 'o', 'ô': 'o', 'ø': 'o', 'ù': 'o', 'ú': 'u', 'ü': 'u', 'û': 'u', 'ñ': 'n', 'ç': 'c', 'ß': 's', 'ÿ': 'y', 'œ': 'o', 'ŕ': 'r', 'ś': 's', 'ń': 'n', 'ṕ': 'p', 'ẃ': 'w', 'ǵ': 'g', 'ǹ': 'n', 'ḿ': 'm', 'ǘ': 'u', 'ẍ': 'x', 'ź': 'z', 'ḧ': 'h', '·': '-', '/': '-', '_': '-', ',': '-', ':': '-', ';': '-', 'З': '3', 'з': '3' }; - - public fromHeading(heading: string): Slug { - const slugifiedHeading = encodeURI(heading.trim() - .toLowerCase() - .replace(/./g, c => this.specialChars[c] || c) - .replace(/[\]\[\!\'\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`]/g, '') - .replace(/\s+/g, '-') // Replace whitespace with - - .replace(/[^\w\-]+/g, '') // Remove remaining non-word chars - .replace(/^\-+/, '') // Remove leading - - .replace(/\-+$/, '') // Remove trailing - +export const githubSlugifier: Slugifier = new class implements Slugifier { + fromHeading(heading: string): Slug { + const slugifiedHeading = encodeURI( + heading.trim() + .toLowerCase() + .replace(/\s+/g, '-') // Replace whitespace with - + .replace(/[\]\[\!\'\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`]/g, '') // Remove known puctuators + .replace(/^\-+/, '') // Remove leading - + .replace(/\-+$/, '') // Remove trailing - ); return new Slug(slugifiedHeading); } -}; \ No newline at end of file +}; diff --git a/extensions/markdown-language-features/src/tableOfContentsProvider.ts b/extensions/markdown-language-features/src/tableOfContentsProvider.ts index c13a52c60d2fe3708d54dd9c15f5570f14967747..30b9de8574460028fbd3af1d870a949e3f3f7e27 100644 --- a/extensions/markdown-language-features/src/tableOfContentsProvider.ts +++ b/extensions/markdown-language-features/src/tableOfContentsProvider.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { MarkdownEngine } from './markdownEngine'; -import { Slug, stripSlugifier } from './slugify'; +import { Slug, githubSlugifier } from './slugify'; export interface TocEntry { readonly slug: Slug; @@ -36,7 +36,7 @@ export class TableOfContentsProvider { public async lookup(fragment: string): Promise { const toc = await this.getToc(); - const slug = stripSlugifier.fromHeading(fragment); + const slug = githubSlugifier.fromHeading(fragment); return toc.find(entry => entry.slug.equals(slug)); } @@ -48,7 +48,7 @@ export class TableOfContentsProvider { const lineNumber = heading.map[0]; const line = document.lineAt(lineNumber); toc.push({ - slug: stripSlugifier.fromHeading(line.text), + slug: githubSlugifier.fromHeading(line.text), text: TableOfContentsProvider.getHeaderText(line.text), level: TableOfContentsProvider.getHeaderLevel(heading.markup), line: lineNumber, diff --git a/extensions/markdown-language-features/src/test/engine.ts b/extensions/markdown-language-features/src/test/engine.ts index a7a30a89ce06dd49259f473d2eb627e1c66a9aa7..860bafad7cc5719578f718452f2841afa1836a8e 100644 --- a/extensions/markdown-language-features/src/test/engine.ts +++ b/extensions/markdown-language-features/src/test/engine.ts @@ -6,13 +6,15 @@ import * as vscode from 'vscode'; import { MarkdownEngine } from '../markdownEngine'; import { MarkdownContributions } from '../markdownExtensions'; +import { githubSlugifier } from '../slugify'; + +const emptyContributions = new class implements MarkdownContributions { + readonly previewScripts: vscode.Uri[] = []; + readonly previewStyles: vscode.Uri[] = []; + readonly previewResourceRoots: vscode.Uri[] = []; + readonly markdownItPlugins: Promise<(md: any) => any>[] = []; +}; export function createNewMarkdownEngine(): MarkdownEngine { - return new MarkdownEngine(new class implements MarkdownContributions { - readonly previewScripts: vscode.Uri[] = []; - readonly previewStyles: vscode.Uri[] = []; - readonly previewResourceRoots: vscode.Uri[] = []; - readonly markdownItPlugins: Promise<(md: any) => any>[] = []; - }); + return new MarkdownEngine(emptyContributions, githubSlugifier); } - diff --git a/extensions/markdown-language-features/src/test/tableOfContentsProvider.test.ts b/extensions/markdown-language-features/src/test/tableOfContentsProvider.test.ts index d2f6c180b69df5291a43505583f2b6531d7f9b60..6d6f382ae7e9c27f6ab6e25d05f98b76a9c9576d 100644 --- a/extensions/markdown-language-features/src/test/tableOfContentsProvider.test.ts +++ b/extensions/markdown-language-features/src/test/tableOfContentsProvider.test.ts @@ -75,18 +75,35 @@ suite('markdown.TableOfContentsProvider', () => { assert.strictEqual(await provider.lookup('fo o'), undefined); }); - test('should normalize special characters #44779', async () => { + test('should handle special characters #44779', async () => { const doc = new InMemoryDocument(testFileName, `# Indentação\n`); const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc); - assert.strictEqual((await provider.lookup('indentacao'))!.line, 0); + assert.strictEqual((await provider.lookup('indentação'))!.line, 0); }); - test('should map special З, #37079', async () => { - const doc = new InMemoryDocument(testFileName, `### Заголовок Header 3`); + test('should handle special characters 2, #48482', async () => { + const doc = new InMemoryDocument(testFileName, `# Инструкция - Делай Раз, Делай Два\n`); const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc); - assert.strictEqual((await provider.lookup('Заголовок-header-3'))!.line, 0); - assert.strictEqual((await provider.lookup('3аголовок-header-3'))!.line, 0); + assert.strictEqual((await provider.lookup('инструкция---делай-раз-делай-два'))!.line, 0); + }); + + test('should handle special characters 3, #37079', async () => { + const doc = new InMemoryDocument(testFileName, `## Header 2 +### Header 3 +## Заголовок 2 +### Заголовок 3 +### Заголовок Header 3 +## Заголовок`); + + const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc); + + assert.strictEqual((await provider.lookup('header-2'))!.line, 0); + assert.strictEqual((await provider.lookup('header-3'))!.line, 1); + assert.strictEqual((await provider.lookup('Заголовок-2'))!.line, 2); + assert.strictEqual((await provider.lookup('Заголовок-3'))!.line, 3); + assert.strictEqual((await provider.lookup('Заголовок-header-3'))!.line, 4); + assert.strictEqual((await provider.lookup('Заголовок'))!.line, 5); }); }); diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index 45814e12d24d6b98f0f284268c26935fcf801218..e7c4cb9d9c35ea63b5b1c14fa3176a25948df65f 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -52,6 +52,13 @@ export function dirname(resource: uri): uri { }); } +export function joinPath(resource: uri, pathFragment: string): uri { + const joinedPath = paths.join(resource.path || '/', pathFragment); + return resource.with({ + path: joinedPath + }); +} + export function distinctParents(items: T[], resourceAccessor: (item: T) => uri): T[] { const distinctParents: T[] = []; for (let i = 0; i < items.length; i++) { diff --git a/src/vs/base/node/stats.ts b/src/vs/base/node/stats.ts index e275f3198e7f5d39753c2bb26946710fb9dc8601..e733e308e70309374e341432bf68007b380ee1e9 100644 --- a/src/vs/base/node/stats.ts +++ b/src/vs/base/node/stats.ts @@ -38,21 +38,26 @@ export function collectLaunchConfigs(folder: string): Promise .issue > .issue-state { background-color: ${styles.inputBackground}; }`); + content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { background-color: ${styles.inputBackground}; }`); } if (styles.inputBorder) { @@ -160,7 +160,7 @@ export class IssueReporter extends Disposable { } if (styles.inputForeground) { - content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state { color: ${styles.inputForeground}; }`); + content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { color: ${styles.inputForeground}; }`); } if (styles.inputErrorBorder) { diff --git a/src/vs/code/electron-browser/issue/media/issueReporter.css b/src/vs/code/electron-browser/issue/media/issueReporter.css index 00e61ad81830ab539fa4aa249f47a55d5fe6c180..aeb02c73f2bd814e5226d69fd77698600c612772 100644 --- a/src/vs/code/electron-browser/issue/media/issueReporter.css +++ b/src/vs/code/electron-browser/issue/media/issueReporter.css @@ -10,18 +10,17 @@ table { width: 100%; max-width: 100%; - margin-bottom: 1rem; background-color: transparent; border-collapse: collapse; } th { vertical-align: bottom; - border-bottom: 2px solid #e9ecef; - padding: .75rem; + border-bottom: 1px solid; + padding: 5px; text-align: inherit; } td { - padding: .25rem; + padding: 5px; vertical-align: top; } @@ -65,6 +64,7 @@ textarea { width: auto; padding: 4px 10px; align-self: flex-end; + margin-bottom: 10px; } select { @@ -75,7 +75,6 @@ select { line-height: 1.5; color: #495057; background-color: #fff; - border-radius: 0.25rem; border: none; } @@ -95,7 +94,7 @@ html { body { margin: 0; - overflow: scroll; + overflow-y: scroll; height: 100%; } @@ -109,16 +108,16 @@ body { .block .block-info { width: 100%; - font-family: 'Menlo', 'Courier New', 'Courier', monospace; + font-family: Monaco, Menlo, Consolas, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback"; font-size: 12px; overflow: auto; overflow-wrap: break-word; + margin: 5px; + padding: 10px; } -pre { - margin: 10px 20px; -} + pre code { - font-family: 'Menlo', 'Courier New', 'Courier', monospace; + font-family: Monaco, Menlo, Consolas, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback"; } #issue-reporter { @@ -126,6 +125,7 @@ pre code { margin-left: auto; margin-right: auto; padding-top: 2em; + padding-bottom: 2em; display: flex; flex-direction: column; height: 100%; @@ -203,7 +203,6 @@ input:disabled { .instructions { font-size: 12px; - margin-left: 1em; margin-top: .5em; } @@ -242,7 +241,7 @@ a { } .section .input-group .validation-error { - margin-left: 13%; + margin-left: 15%; } .section .inline-form-control, .section .inline-label { @@ -262,17 +261,25 @@ a { } #similar-issues { - margin-left: 13%; + margin-left: 15%; display: block; } +#problem-source-help-text { + margin-left: calc(15% + 1em); +} + @media (max-width: 950px) { .section .inline-label { - width: 13%; + width: 15%; + } + + #problem-source-help-text { + margin-left: calc(15% + 1em); } .section .inline-form-control { - width: calc(87% - 5px); + width: calc(85% - 5px); } } @@ -281,6 +288,10 @@ a { display: none !important; } + #problem-source-help-text { + margin-left: 1em; + } + .section .inline-form-control { width: 100%; } diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index c5c8d20d48b649041cd019194bbdcf7944d1a466..2aae0897c8b6c50672a5e6814a9f0b717b413db6 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -638,7 +638,7 @@ export class SuggestWidget implements IContentWidget, IDelegate this.show(); break; case State.Open: - hide(this.messageElement, this.details.element); + hide(this.messageElement); show(this.listElement); this.show(); break; @@ -677,7 +677,10 @@ export class SuggestWidget implements IContentWidget, IDelegate this.loadingTimeout = null; } - this.completionModel = completionModel; + if (this.completionModel !== completionModel) { + this.completionModel = completionModel; + this.focusedItem = null; + } if (isFrozen && this.state !== State.Empty && this.state !== State.Hidden) { this.setState(State.Frozen); @@ -712,7 +715,6 @@ export class SuggestWidget implements IContentWidget, IDelegate */ this.telemetryService.publicLog('suggestWidget', { ...stats, ...this.editor.getTelemetryData() }); - this.focusedItem = null; this.list.splice(0, this.list.length, this.completionModel.items); if (isFrozen) { diff --git a/src/vs/platform/issue/electron-main/issueService.ts b/src/vs/platform/issue/electron-main/issueService.ts index d8eceb42c9281255b9abd73ec6722040e873b3ed..7723353fc8724972e9e857a48732a2f11c1cfd9c 100644 --- a/src/vs/platform/issue/electron-main/issueService.ts +++ b/src/vs/platform/issue/electron-main/issueService.ts @@ -51,7 +51,7 @@ export class IssueService implements IIssueService { }); this._issueParentWindow = BrowserWindow.getFocusedWindow(); - const position = this.getWindowPosition(this._issueParentWindow, 800, 900); + const position = this.getWindowPosition(this._issueParentWindow, 700, 800); this._issueWindow = new BrowserWindow({ width: position.width, height: position.height, diff --git a/src/vs/platform/localizations/common/localizations.ts b/src/vs/platform/localizations/common/localizations.ts index 5f091c4f6f35d3c671cb086a3029226bd27e84b7..b6361b620a47cb08b48cbd89e41e3afe10d5e7e0 100644 --- a/src/vs/platform/localizations/common/localizations.ts +++ b/src/vs/platform/localizations/common/localizations.ts @@ -13,6 +13,7 @@ export interface ILocalization { languageName?: string; languageNameLocalized?: string; translations: ITranslation[]; + minimalTranslations?: { [key: string]: string }; } export interface ITranslation { diff --git a/src/vs/platform/node/minimalTranslations.ts b/src/vs/platform/node/minimalTranslations.ts new file mode 100644 index 0000000000000000000000000000000000000000..11248d86d88e3dc8cd1d6e0b721a03006019c3d1 --- /dev/null +++ b/src/vs/platform/node/minimalTranslations.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; + +// The strings localized in this file will get pulled into the manifest of the language packs. +// So that they are available for VS Code to use without downloading the entire language pack. + +export const minimumTranslatedStrings = { + showLanguagePackExtensions: localize('showLanguagePackExtensions', "The Marketplace has extensions that can localize VS Code in the {0} language"), + searchMarketplace: localize('searchMarketplace', "Search Marketplace"), + installAndRestartMessage: localize('installAndRestartMessage', "Install language pack to localize VS Code in {0} language. Restart VS Code after installing for the language to take effect."), + installAndRestart: localize('installAndRestart', "Install and Restart"), + install: localize('install', 'Install') +}; + diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 9199efd1eec718f273ca00e5e20d2d06ec948979..27acdda3489a33c965d731c0e1211206806d1741 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -5266,6 +5266,23 @@ declare module 'vscode' { readonly webviewPanel: WebviewPanel; } + /** + * Restore webview panels that have been persisted when vscode shuts down. + */ + interface WebviewPanelSerializer { + /** + * Restore a webview panel from its seriailzed `state`. + * + * Called when a serialized webview first becomes visible. + * + * @param webviewPanel Webview panel to restore. The serializer should take ownership of this panel. + * @param state Persisted state. This state comesfrom the value set inside the webview by `acquireVsCodeApi().setState`. + * + * @return Thanble indicating that the webview has been fully restored. + */ + deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any): Thenable; + } + /** * Namespace describing the environment the editor runs in. */ @@ -5760,6 +5777,19 @@ declare module 'vscode' { */ export function createWebviewPanel(viewType: string, title: string, showOptions: ViewColumn | { viewColumn: ViewColumn, preserveFocus?: boolean }, options?: WebviewPanelOptions & WebviewOptions): WebviewPanel; + /** + * Registers a [webview panel serializer](#WebviewPanelSerializer). + * + * Extensions that support reviving should have an `"onWebviewPanel:viewType"` activation method and + * make sure that [registerWebviewPanelSerializer](#registerWebviewPanelSerializer) is called during activation. + * + * Only a single serializer may be registered at a time for a given `viewType`. + * + * @param viewType Type of the webview panel that can be serialized. + * @param reviver Webview serializer. + */ + export function registerWebviewPanelSerializer(viewType: string, reviver: WebviewPanelSerializer): Disposable; + /** * Set a message to the status bar. This is a short hand for the more powerful * status bar [items](#window.createStatusBarItem). diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index b7c2346400e7efe94ea5912bae2e6d5246679622..fe241ee9ade3ff7f9483364f216f8fce72fee415 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -296,42 +296,6 @@ declare module 'vscode' { //#endregion - //#region Matt: WebView Serializer - - /** - * Restore webview panels that have been persisted when vscode shuts down. - */ - interface WebviewPanelSerializer { - /** - * Restore a webview panel from its seriailzed `state`. - * - * Called when a serialized webview first becomes visible. - * - * @param webviewPanel Webview panel to restore. The serializer should take ownership of this panel. - * @param state Persisted state. - * - * @return Thanble indicating that the webview has been fully restored. - */ - deserializeWebviewPanel(webviewPanel: WebviewPanel, state: any): Thenable; - } - - namespace window { - /** - * Registers a webview panel serializer. - * - * Extensions that support reviving should have an `"onView:viewType"` activation method and - * make sure that [registerWebviewPanelSerializer](#registerWebviewPanelSerializer) is called during activation. - * - * Only a single serializer may be registered at a time for a given `viewType`. - * - * @param viewType Type of the webview panel that can be serialized. - * @param reviver Webview serializer. - */ - export function registerWebviewPanelSerializer(viewType: string, reviver: WebviewPanelSerializer): Disposable; - } - - //#endregion - //#region Tasks /** diff --git a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts index cb968927ce33a694597b36ee5a81cc67a0ca2f74..ae00eadde446749caab57af0c9f662e42b65ea17 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWebview.ts @@ -137,7 +137,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv reviveWebview(webview: WebviewEditorInput): TPromise { const viewType = webview.state.viewType; - return this._extensionService.activateByEvent(`onView:${viewType}`).then(() => { + return this._extensionService.activateByEvent(`onWebviewPanel:${viewType}`).then(() => { const handle = 'revival-' + MainThreadWebviews.revivalPool++; this._webviews.set(handle, webview); webview._events = this.createWebviewEventDelegate(handle); diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index ff1eec77d1218fe232293f18ac35f2abad4714f3..31ec131d4aa89749a41a0b5f5d25fa6ab84ecdfa 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -422,6 +422,9 @@ export function createApiFactory( createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, options: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel { return extHostWebviews.createWebview(viewType, title, showOptions, options, extension.extensionLocation); }, + registerWebviewPanelSerializer(viewType: string, serializer: vscode.WebviewPanelSerializer) { + return extHostWebviews.registerWebviewPanelSerializer(viewType, serializer); + }, createTerminal(nameOrOptions: vscode.TerminalOptions | string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { if (typeof nameOrOptions === 'object') { return extHostTerminalService.createTerminalFromOptions(nameOrOptions); @@ -441,9 +444,6 @@ export function createApiFactory( registerDecorationProvider: proposedApiFunction(extension, (provider: vscode.DecorationProvider) => { return extHostDecorations.registerDecorationProvider(provider, extension.id); }), - registerWebviewPanelSerializer: proposedApiFunction(extension, (viewType: string, serializer: vscode.WebviewPanelSerializer) => { - return extHostWebviews.registerWebviewPanelSerializer(viewType, serializer); - }), registerProtocolHandler: proposedApiFunction(extension, (handler: vscode.ProtocolHandler) => { return extHostUrls.registerProtocolHandler(extension.id, handler); }) diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index 4c0370d20d8dcfb362419194a9bf41cf256ac5be..f2666010f655a5c937a23b6da7165f0f24e913e2 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -18,6 +18,7 @@ import { ICachedSearchStats, IFileMatch, IFolderQuery, IPatternInfo, IRawSearchQ import * as vscode from 'vscode'; import { ExtHostSearchShape, IMainContext, MainContext, MainThreadSearchShape } from './extHost.protocol'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { joinPath } from 'vs/base/common/resources'; type OneOrMore = T | T[]; @@ -151,7 +152,7 @@ class TextSearchResultsCollector { } if (!this._currentFileMatch) { - const resource = URI.file(path.join(this.folderQueries[folderIdx].folder.fsPath, data.path)); + const resource = joinPath(this.folderQueries[folderIdx].folder, data.path); this._currentFileMatch = { resource, lineMatches: [] @@ -271,7 +272,7 @@ class BatchedCollector { } interface IDirectoryEntry { - base: string; + base: URI; relativePath: string; basename: string; } @@ -282,8 +283,8 @@ interface IDirectoryTree { } interface IInternalFileMatch { - base?: string; - relativePath: string; // Not necessarily relative... extraFiles put an absolute path here. Rename. + base: URI; + relativePath?: string; // Not present for extraFiles or absolute path matches basename: string; size?: number; } @@ -433,9 +434,9 @@ class TextSearchEngine { const testingPs = []; const progress = { report: (result: vscode.TextSearchResult) => { - const siblingFn = () => { + const siblingFn = folderQuery.folder.scheme === 'file' && (() => { return this.readdir(path.dirname(path.join(folderQuery.folder.fsPath, result.path))); - }; + }); testingPs.push( queryTester.includedInQuery(result.path, path.basename(result.path), siblingFn) @@ -558,7 +559,7 @@ class FileSearchEngine { // Report result from file pattern if matching if (exists) { onResult({ - relativePath: this.filePattern, + base: URI.file(this.filePattern), basename: path.basename(this.filePattern), size }); @@ -572,15 +573,15 @@ class FileSearchEngine { // For each extra file if (this.config.extraFileResources) { this.config.extraFileResources - .map(uri => uri.toString()) - .forEach(extraFilePath => { - const basename = path.basename(extraFilePath); - if (this.globalExcludePattern && this.globalExcludePattern(extraFilePath, basename)) { + .forEach(extraFile => { + const extraFileStr = extraFile.toString(); // ? + const basename = path.basename(extraFileStr); + if (this.globalExcludePattern && this.globalExcludePattern(extraFileStr, basename)) { return; // excluded } // File: Check for match on file pattern and include pattern - this.matchFile(onResult, { relativePath: extraFilePath /* no workspace relative path */, basename }); + this.matchFile(onResult, { base: extraFile, basename }); }); } @@ -604,7 +605,6 @@ class FileSearchEngine { let cancellation = new CancellationTokenSource(); return new PPromise((resolve, reject, onResult) => { const options = this.getSearchOptionsForFolder(fq); - const folderStr = fq.folder.fsPath; let filePatternSeen = false; const tree = this.initDirectoryTree(); @@ -616,20 +616,19 @@ class FileSearchEngine { return; } - // This is slow... if (noSiblingsClauses) { if (relativePath === this.filePattern) { filePatternSeen = true; } const basename = path.basename(relativePath); - this.matchFile(onResult, { base: folderStr, relativePath, basename }); + this.matchFile(onResult, { base: fq.folder, relativePath, basename }); return; } // TODO: Optimize siblings clauses with ripgrep here. - this.addDirectoryEntries(tree, folderStr, relativePath, onResult); + this.addDirectoryEntries(tree, fq.folder, relativePath, onResult); }; new TPromise(resolve => process.nextTick(resolve)) @@ -646,10 +645,10 @@ class FileSearchEngine { if (noSiblingsClauses && this.isLimitHit) { if (!filePatternSeen) { // If the limit was hit, check whether filePattern is an exact relative match because it must be included - return this.checkFilePatternRelativeMatch(folderStr).then(({ exists, size }) => { + return this.checkFilePatternRelativeMatch(fq.folder).then(({ exists, size }) => { if (exists) { onResult({ - base: folderStr, + base: fq.folder, relativePath: this.filePattern, basename: path.basename(this.filePattern), }); @@ -658,7 +657,7 @@ class FileSearchEngine { } } - this.matchDirectoryTree(tree, folderStr, queryTester, onResult); + this.matchDirectoryTree(tree, queryTester, onResult); return null; }).then( () => { @@ -694,7 +693,7 @@ class FileSearchEngine { return tree; } - private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: string, relativeFile: string, onResult: (result: IInternalFileMatch) => void) { + private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: URI, relativeFile: string, onResult: (result: IInternalFileMatch) => void) { // Support relative paths to files from a root resource (ignores excludes) if (relativeFile === this.filePattern) { const basename = path.basename(this.filePattern); @@ -719,7 +718,7 @@ class FileSearchEngine { add(relativeFile); } - private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, rootFolder: string, queryTester: QueryGlobTester, onResult: (result: IInternalFileMatch) => void) { + private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, queryTester: QueryGlobTester, onResult: (result: IInternalFileMatch) => void) { const self = this; const filePattern = this.filePattern; function matchDirectory(entries: IDirectoryEntry[]) { @@ -794,12 +793,12 @@ class FileSearchEngine { }); } - private checkFilePatternRelativeMatch(basePath: string): TPromise<{ exists: boolean, size?: number }> { - if (!this.filePattern || path.isAbsolute(this.filePattern)) { + private checkFilePatternRelativeMatch(base: URI): TPromise<{ exists: boolean, size?: number }> { + if (!this.filePattern || path.isAbsolute(this.filePattern) || base.scheme !== 'file') { return TPromise.wrap({ exists: false }); } - const absolutePath = path.join(basePath, this.filePattern); + const absolutePath = path.join(base.fsPath, this.filePattern); return this._pfs.stat(absolutePath).then(stat => { return { exists: !stat.isDirectory(), @@ -894,7 +893,7 @@ class FileSearchManager { private rawMatchToSearchItem(match: IInternalFileMatch): IFileMatch { return { - resource: URI.file(match.base ? path.join(match.base, match.relativePath) : match.relativePath) + resource: joinPath(match.base, match.relativePath) }; } diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index 8fcfc3f1c499235c2048895ed259d8da0f1d1d49..40431d62f5e8acc00e73f15d7aef91c8f02ed8d4 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -63,8 +63,9 @@ export class CustomViewsService extends Disposable implements IViewsService { return this.viewletService.openViewlet(viewletDescriptor.id) .then((viewlet: IViewsViewlet) => { if (viewlet && viewlet.openView) { - viewlet.openView(id, focus); + return viewlet.openView(id, focus); } + return null; }); } } @@ -280,17 +281,21 @@ class CustomTreeViewer extends Disposable implements ITreeViewer { reveal(item: ITreeItem, parentChain: ITreeItem[], options?: { select?: boolean }): TPromise { if (this.tree && this.isVisible) { options = options ? options : { select: true }; - const select = isUndefinedOrNull(options.select) ? true : options.select; - var result = TPromise.as(null); - parentChain.forEach((e) => { - result = result.then(() => this.tree.expand(e)); - }); - return result.then(() => this.tree.reveal(item)) - .then(() => { - if (select) { - this.tree.setSelection([item], { source: 'api' }); - } + const root: Root = this.tree.getInput(); + const promise = root.children ? TPromise.as(null) : this.refresh(); // Refresh if root is not populated + return promise.then(() => { + const select = isUndefinedOrNull(options.select) ? true : options.select; + var result = TPromise.as(null); + parentChain.forEach((e) => { + result = result.then(() => this.tree.expand(e)); }); + return result.then(() => this.tree.reveal(item)) + .then(() => { + if (select) { + this.tree.setSelection([item], { source: 'api' }); + } + }); + }); } return TPromise.as(null); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index 8736c448bc0e546bc274130beda41e17b4be9492..315700baf842e468b2171ca70e0e1f0e7db502f9 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts @@ -34,7 +34,6 @@ import { getHashedRemotesFromUri } from 'vs/workbench/parts/stats/node/workspace import { IRequestService } from 'vs/platform/request/node/request'; import { asJson } from 'vs/base/node/request'; import { isNumber } from 'vs/base/common/types'; -import { language, LANGUAGE_DEFAULT } from 'vs/base/common/platform'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -46,7 +45,6 @@ const empty: { [key: string]: any; } = Object.create(null); const milliSecondsInADay = 1000 * 60 * 60 * 24; const choiceNever = localize('neverShowAgain', "Don't Show Again"); const searchMarketplace = localize('searchMarketplace', "Search Marketplace"); -const coreLanguages = ['de', 'es', 'fr', 'it', 'ja', 'ko', 'ru', 'tr', 'zh-cn', 'zh-tw']; interface IDynamicWorkspaceRecommendations { remoteSet: string[]; @@ -93,7 +91,6 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe this._extensionsRecommendationsUrl = product.extensionsGallery.recommendationsUrl; } - this.getLanguageExtensionRecommendations(); this.getCachedDynamicWorkspaceRecommendations(); this._suggestFileBasedRecommendations(); this.promptWorkspaceRecommendationsPromise = this._suggestWorkspaceRecommendations(); @@ -132,90 +129,6 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return this._galleryService.isEnabled() && !this.environmentService.extensionDevelopmentPath; } - private getLanguageExtensionRecommendations() { - const config = this.configurationService.getValue(ConfigurationKey); - const languagePackSuggestionIgnoreList = JSON.parse(this.storageService.get - ('extensionsAssistant/languagePackSuggestionIgnore', StorageScope.GLOBAL, '[]')); - - if (!language - || language === LANGUAGE_DEFAULT - || coreLanguages.some(x => language === x || language.indexOf(x + '-') === 0) - || config.ignoreRecommendations - || config.showRecommendationsOnlyOnDemand - || languagePackSuggestionIgnoreList.indexOf(language) > -1) { - return; - } - - this.extensionsService.getInstalled(LocalExtensionType.User).then(locals => { - for (var i = 0; i < locals.length; i++) { - if (locals[i].manifest - && locals[i].manifest.contributes - && Array.isArray(locals[i].manifest.contributes.localizations) - && locals[i].manifest.contributes.localizations.some(x => x.languageId === language)) { - return; - } - } - - this._galleryService.query({ text: `tag:lp-${language}` }).then(pager => { - if (!pager || !pager.firstPage || !pager.firstPage.length) { - return; - } - - this.notificationService.prompt( - Severity.Info, - localize('showLanguagePackExtensions', "The Marketplace has extensions that can help localizing VS Code to '{0}' locale", language), - [{ - label: searchMarketplace, - run: () => { - /* __GDPR__ - "languagePackSuggestion:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('languagePackSuggestion:popup', { userReaction: 'ok', language }); - this.viewletService.openViewlet('workbench.view.extensions', true) - .then(viewlet => viewlet as IExtensionsViewlet) - .then(viewlet => { - viewlet.search(`tag:lp-${language}`); - viewlet.focus(); - }); - } - }, - { - label: choiceNever, - isSecondary: true, - run: () => { - languagePackSuggestionIgnoreList.push(language); - this.storageService.store( - 'extensionsAssistant/languagePackSuggestionIgnore', - JSON.stringify(languagePackSuggestionIgnoreList), - StorageScope.GLOBAL - ); - /* __GDPR__ - "languagePackSuggestion:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('languagePackSuggestion:popup', { userReaction: 'neverShowAgain', language }); - } - }], - () => { - /* __GDPR__ - "languagePackSuggestion:popup" : { - "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "language": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('languagePackSuggestion:popup', { userReaction: 'cancelled', language }); - } - ); - }); - }); - } - - getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason, reasonText: string }; } { let output: { [id: string]: { reasonId: ExtensionRecommendationReason, reasonText: string }; } = Object.create(null); 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 7e9a9f3ffe4a8890cda60b55713047f68800e364..6bc4493acf9802fbce70945300038e9e8c11bd11 100644 --- a/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts +++ b/src/vs/workbench/parts/localizations/electron-browser/localizations.contribution.ts @@ -23,11 +23,12 @@ 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, StorageScope, } 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'; +import { minimumTranslatedStrings } from 'vs/platform/node/minimalTranslations'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; // Register action to configure locale and related settings const registry = Registry.as(Extensions.WorkbenchActions); @@ -43,7 +44,8 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo @IStorageService private storageService: IStorageService, @IExtensionManagementService private extensionManagementService: IExtensionManagementService, @IExtensionGalleryService private galleryService: IExtensionGalleryService, - @IViewletService private viewletService: IViewletService + @IViewletService private viewletService: IViewletService, + @ITelemetryService private telemetryService: ITelemetryService ) { super(); this.updateLocaleDefintionSchema(); @@ -96,6 +98,7 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo private checkAndInstall(): void { const language = platform.language; + const locale = platform.locale; if (language !== 'en' && language !== 'en_us') { this.isLanguageInstalled(language) .then(installed => { @@ -115,53 +118,116 @@ export class LocalizationWorkbenchContribution extends Disposable implements IWo return; } - const bundledTranslations = (product['bundledTranslations'] || {})[platform.locale]; - if (language === platform.locale || !bundledTranslations || !bundledTranslations['languageName']) { + const languagePackSuggestionIgnoreList = JSON.parse(this.storageService.get + ('extensionsAssistant/languagePackSuggestionIgnore', StorageScope.GLOBAL, '[]')); + + if (language === locale || languagePackSuggestionIgnoreList.indexOf(language) > -1) { 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"); + this.isLanguageInstalled(locale) + .then(installed => { + if (installed) { + return; + } - searchForLanguagePacks = bundledTranslations['searchForLanguagePacks']; - searchMarketplace = bundledTranslations['searchMarketplace']; - dontShowAgain = bundledTranslations['neverAgain']; + const ceintlExtensionSearch = this.galleryService.query({ names: [`MS-CEINTL.vscode-language-pack-${locale}`], pageSize: 1 }); + const tagSearch = this.galleryService.query({ text: `tag:lp-${locale}`, pageSize: 1 }); - const dontShowSearchLanguagePacksAgainKey = 'language.install.donotask'; - let dontShowSearchForLanguages = JSON.parse(this.storageService.get(dontShowSearchLanguagePacksAgainKey, StorageScope.GLOBAL, '[]')); - if (!Array.isArray(dontShowSearchForLanguages)) { - dontShowSearchForLanguages = []; - } + TPromise.join([ceintlExtensionSearch, tagSearch]).then(([ceintlResult, tagResult]) => { + if (ceintlResult.total === 0 && tagResult.total === 0) { + return; + } - if (dontShowSearchForLanguages.indexOf(platform.locale) > -1 - || !searchForLanguagePacks - || !searchMarketplace - || !dontShowAgain) { - return; - } + const extensionToInstall = ceintlResult.total === 1 ? ceintlResult.firstPage[0] : tagResult.total === 1 ? tagResult.firstPage[0] : null; + const extensionToFetchTranslationsFrom = extensionToInstall || tagResult.total > 0 ? tagResult.firstPage[0] : null; - 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(); - }); + if (!extensionToFetchTranslationsFrom || !extensionToFetchTranslationsFrom.assets.manifest) { + return; } - }, - { - label: dontShowAgain, run: () => { - dontShowSearchForLanguages.push(language); - this.storageService.store(dontShowSearchLanguagePacksAgainKey, StorageScope.GLOBAL, dontShowSearchForLanguages); - } - } - ]); + + this.galleryService.getManifest(extensionToFetchTranslationsFrom).then(x => { + if (!x.contributes || !x.contributes.localizations) { + return; + } + const locContribution = x.contributes.localizations.filter(x => x.languageId.toLowerCase() === locale)[0]; + if (!locContribution) { + return; + } + + const translations = { + ...minimumTranslatedStrings, + ...(locContribution.minimalTranslations || {}) + }; + + const logUserReaction = (userReaction: string) => { + /* __GDPR__ + "languagePackSuggestion:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "language": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('languagePackSuggestion:popup', { userReaction, language }); + }; + + const searchAction = { + label: translations['searchMarketplace'], + run: () => { + logUserReaction('search'); + this.viewletService.openViewlet(EXTENSIONS_VIEWLET_ID, true) + .then(viewlet => viewlet as IExtensionsViewlet) + .then(viewlet => { + viewlet.search(`tag:lp-${locale}`); + viewlet.focus(); + }); + } + }; + + const installAction = { + label: translations['install'], + run: () => { + logUserReaction('install'); + this.installExtension(extensionToInstall); + } + }; + + const installAndRestartAction = { + label: translations['installAndRestart'], + run: () => { + logUserReaction('installAndRestart'); + this.installExtension(extensionToInstall).then(() => this.windowsService.relaunch({})); + } + }; + + const mainActions = extensionToInstall ? [installAndRestartAction, installAction] : [searchAction]; + const promptMessage = translations[extensionToInstall ? 'installAndRestartMessage' : 'showLanguagePackExtensions'] + .replace('{0}', locContribution.languageNameLocalized || locContribution.languageName || locale); + + this.notificationService.prompt( + Severity.Info, + promptMessage, + [...mainActions, + { + label: localize('neverAgain', "Don't Show Again"), + isSecondary: true, + run: () => { + languagePackSuggestionIgnoreList.push(language); + this.storageService.store( + 'extensionsAssistant/languagePackSuggestionIgnore', + JSON.stringify(languagePackSuggestionIgnoreList), + StorageScope.GLOBAL + ); + logUserReaction('neverShowAgain'); + } + }], + () => { + logUserReaction('cancelled'); + } + ); + + }); + }); + }); } diff --git a/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css index 51a944d9908b521675905993b1a865163900006c..688fc6a36d94d5514f6bfe043fd05e521a2836e3 100644 --- a/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css @@ -4,14 +4,17 @@ *--------------------------------------------------------------------------------------------*/ .settings-editor { - padding: 11px 0px 0px 27px; + padding-top: 11px; margin: auto; } /* header styling */ - .settings-editor > .settings-header { - padding: 0px 10px 0px 0px; + padding-left: 15px; + padding-right: 5px; + max-width: 800px; + box-sizing: border-box; + margin: auto; } .settings-editor > .settings-header > .settings-preview-header { @@ -22,14 +25,19 @@ 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 { +.settings-editor > .settings-header > .settings-advanced-customization .open-settings-button, +.settings-editor > .settings-header > .settings-advanced-customization .open-settings-button:hover, +.settings-editor > .settings-header > .settings-advanced-customization .open-settings-button:active { padding: 0; text-decoration: underline; display: inline; } +.settings-editor > .settings-header > .settings-advanced-customization { + opacity: .7; + margin-top: 10px; +} + .settings-editor > .settings-header > .settings-preview-header > .settings-preview-warning { text-align: right; text-transform: uppercase; @@ -59,7 +67,7 @@ } .settings-editor > .settings-header > .settings-header-controls { - margin-top: 7px; + margin-top: 2px; height: 30px; display: flex; } @@ -87,6 +95,7 @@ white-space: nowrap; margin-right: 10px; margin-left: 2px; + opacity: 0.7; } .settings-editor > .settings-body .settings-tree-container .monaco-tree-wrapper { @@ -122,16 +131,6 @@ min-height: 75px; } -.settings-editor > .settings-body > .settings-tree-container .monaco-tree-row .content::before { - content: ' '; - display: inline-block; - position: absolute; - width: 5px; - left: -9px; - top: 2px; - bottom: 10px; -} - .settings-editor > .settings-body > .settings-tree-container .setting-item.odd:not(.focused):not(.selected):not(:hover), .settings-editor > .settings-body > .settings-tree-container .monaco-tree:not(:focus) .setting-item.focused.odd:not(.selected):not(:hover), .settings-editor > .settings-body > .settings-tree-container .monaco-tree:not(.focused) .setting-item.focused.odd:not(.selected):not(:hover) { @@ -146,37 +145,37 @@ .settings-editor > .settings-body > .settings-tree-container .setting-item > .setting-item-right { min-width: 180px; - margin: 21px 10px 0px; + margin: 21px 10px 0px 5px; } .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-title { line-height: initial; } +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-title .setting-item-is-configured-label { + font-style: italic; + opacity: 0.8; + margin-right: 7px; +} + .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-title .setting-item-overrides { opacity: 0.5; - margin-left: 7px; font-style: italic; } +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-label { + margin-right: 7px; +} + .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-label, .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-category { font-weight: bold; - font-size: 14px; } .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-category { opacity: 0.7; } -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-key { - margin-left: 10px; - font-family: Monaco, Menlo, Consolas, "Droid Sans Mono", "Inconsolata", "Courier New", monospace, "Droid Sans Fallback"; - font-size: 90%; - opacity: 0.8; - display: none; -} - .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-description { opacity: 0.7; margin-top: 3px; @@ -196,14 +195,6 @@ height: initial; } -.settings-editor > .settings-body > .settings-tree-container .setting-item.is-expandable .setting-item-description { - cursor: pointer; -} - -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value { - display: flex; -} - .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .edit-in-settings-button, .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .edit-in-settings-button:hover, .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .edit-in-settings-button:active { @@ -222,28 +213,16 @@ height: 26px; } -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-value-checkbox { - position: relative; -} - -.settings-editor > .settings-body > .settings-tree-container .setting-item.is-configured .setting-value-checkbox::after { - content: ' '; +.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .setting-reset-button.monaco-button { + text-align: left; display: block; - height: 3px; - width: 18px; - position: absolute; - top: 15px; - left: -3px; + visibility: hidden; + + padding-top: 0px; /* So focus outline doesn't overlap the control above */ } -.settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .setting-reset-button.monaco-button { - display: inline-block; - background: url("clean.svg") center center no-repeat; - width: 16px; - height: 16px; - margin: auto; - margin-left: 3px; - visibility: hidden; +.settings-editor > .settings-body > .settings-tree-container .setting-item.is-configured .setting-item-value > .setting-reset-button.monaco-button { + visibility: visible; } .settings-editor > .settings-body > .settings-tree-container .setting-item .expand-indicator { @@ -259,14 +238,6 @@ visibility: visible; } -.vs-dark .settings-editor > .settings-body > .settings-tree-container .setting-item .setting-item-value > .setting-reset-button.monaco-button { - background: url("clean-dark.svg") center center no-repeat; -} - -.settings-editor > .settings-body > .settings-tree-container .setting-item.is-configured .setting-item-value > .setting-reset-button.monaco-button { - visibility: visible; -} - .settings-editor > .settings-body > .settings-tree-container .all-settings { display: flex; } @@ -292,13 +263,10 @@ overflow: visible; } -.settings-editor .settings-body { - margin-left: -15px; -} - .settings-editor > .settings-body > .settings-tree-container .settings-group-title-label { margin: 0px; padding: 5px 0px; + font-size: 13px; } .settings-editor > .settings-body .settings-feedback-button { diff --git a/src/vs/workbench/parts/preferences/browser/preferencesActions.ts b/src/vs/workbench/parts/preferences/browser/preferencesActions.ts index b40d4055be583a49bdd60fbf08fde0a0ed75ba88..ecf92c2c9bab97ef9e5496fa9d839e1da5bb9c01 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesActions.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesActions.ts @@ -55,7 +55,7 @@ export class OpenRawUserSettingsAction extends Action { export class OpenSettings2Action extends Action { public static readonly ID = 'workbench.action.openSettings2'; - public static readonly LABEL = nls.localize('openSettings2', "Open Settings (Experimental)"); + public static readonly LABEL = nls.localize('openSettings2', "Open Settings (Preview)"); constructor( id: string, diff --git a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts index bf39855a2fa6f0aa55df4011d2cf0df2bb2f1fab..34c8bc9b29ece87a6a5dc6a473662578546bdaf6 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts @@ -20,7 +20,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { WorkbenchTree } from 'vs/platform/list/browser/listService'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry'; +import { editorBackground, foreground, listActiveSelectionBackground, listInactiveSelectionBackground } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; @@ -46,6 +46,7 @@ export class SettingsEditor2 extends BaseEditor { private settingsTargetsWidget: SettingsTargetsWidget; private showConfiguredSettingsOnlyCheckbox: HTMLInputElement; + private savedExpandedGroups: any[]; private settingsTreeContainer: HTMLElement; private settingsTree: WorkbenchTree; @@ -114,17 +115,7 @@ export class SettingsEditor2 extends BaseEditor { 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())); + previewTextLabel.textContent = localize('previewLabel', "This is a preview of our new settings editor"); const searchContainer = DOM.append(this.headerContainer, $('.search-container')); this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, searchContainer, { @@ -139,6 +130,20 @@ export class SettingsEditor2 extends BaseEditor { } })); + const advancedCustomization = DOM.append(this.headerContainer, $('.settings-advanced-customization')); + const advancedCustomizationLabel = DOM.append(advancedCustomization, $('span.settings-advanced-customization-label')); + advancedCustomizationLabel.textContent = localize('advancedCustomizationLabel', "For advanced customizations open and edit") + ' '; + const openSettingsButton = this._register(new Button(advancedCustomization, { 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', "settings.json"); + openSettingsButton.element.classList.add('open-settings-button'); + + this._register(openSettingsButton.onDidClick(() => this.openSettingsFile())); + const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls')); const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container')); this.settingsTargetsWidget = this._register(this.instantiationService.createInstance(SettingsTargetsWidget, targetWidgetContainer)); @@ -186,9 +191,8 @@ export class SettingsEditor2 extends BaseEditor { this.settingsTreeContainer = DOM.append(parent, $('.settings-tree-container')); this.treeDataSource = this.instantiationService.createInstance(SettingsDataSource, this.viewState); - const renderer = this.instantiationService.createInstance(SettingsRenderer, this.viewState, this.settingsTreeContainer); + const renderer = this.instantiationService.createInstance(SettingsRenderer, this.settingsTreeContainer); this._register(renderer.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value))); - this._register(renderer.onDidClickButton(e => this.onDidClickShowAllSettings())); const treeClass = 'settings-editor-tree'; this.settingsTree = this.instantiationService.createInstance(WorkbenchTree, this.settingsTreeContainer, @@ -208,14 +212,14 @@ export class SettingsEditor2 extends BaseEditor { }); this._register(registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { - const activeListBackground = theme.getColor('list.activeSelectionBackground'); - if (activeListBackground) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree.focused .monaco-tree-row.focused .content::before { background-color: ${activeListBackground}; }`); + const activeBorderColor = theme.getColor(listActiveSelectionBackground); + if (activeBorderColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree:focus .monaco-tree-row.focused {outline: solid 1px ${activeBorderColor}; outline-offset: -1px; }`); } - const inactiveListBackground = theme.getColor('list.inactiveSelectionBackground'); - if (inactiveListBackground) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree .monaco-tree-row.focused .content::before { background-color: ${inactiveListBackground}; }`); + const inactiveBorderColor = theme.getColor(listInactiveSelectionBackground); + if (inactiveBorderColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-tree .monaco-tree-row.focused {outline: solid 1px ${inactiveBorderColor}; outline-offset: -1px; }`); } })); @@ -265,6 +269,25 @@ export class SettingsEditor2 extends BaseEditor { private onShowConfiguredOnlyClicked(): void { this.viewState.showConfiguredOnly = this.showConfiguredSettingsOnlyCheckbox.checked; this.refreshTree(); + + // TODO@roblou - This is slow + if (this.viewState.showConfiguredOnly) { + this.savedExpandedGroups = this.settingsTree.getExpandedElements(); + const nav = this.settingsTree.getNavigator(); + let element; + while (element = nav.next()) { + this.settingsTree.expand(element); + } + } else if (this.savedExpandedGroups) { + const nav = this.settingsTree.getNavigator(); + let element; + while (element = nav.next()) { + this.settingsTree.collapse(element); + } + + this.settingsTree.expandAll(this.savedExpandedGroups); + this.savedExpandedGroups = null; + } } private onDidChangeSetting(key: string, value: any): void { @@ -292,11 +315,6 @@ export class SettingsEditor2 extends BaseEditor { this.delayedModifyLogging.trigger(() => this.reportModifiedSetting(reportModifiedProps)); } - private onDidClickShowAllSettings(): void { - this.viewState.showAllSettings = !this.viewState.showAllSettings; - this.refreshTree(); - } - private reportModifiedSetting(props: { key: string, query: string, searchResults: ISearchResult[], rawResults: ISearchResult[], showConfiguredOnly: boolean, isReset: boolean, settingsTarget: SettingsTarget }): void { this.pendingSettingModifiedReport = null; diff --git a/src/vs/workbench/parts/preferences/browser/settingsTree.ts b/src/vs/workbench/parts/preferences/browser/settingsTree.ts index 30b3174d4dce51c10f01237516832889b71026f0..02462d73598220031041741f08ff7d5c05be7d2f 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsTree.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { Button } from 'vs/base/browser/ui/button/button'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; @@ -12,19 +13,19 @@ import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; +import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAccessibilityProvider, IDataSource, IFilter, IRenderer, ITree } from 'vs/base/parts/tree/browser/tree'; import { localize } from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; -import { registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { editorActiveLinkForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { SettingsTarget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; import { ISearchResult, ISetting, ISettingsGroup } from 'vs/workbench/services/preferences/common/preferences'; import { DefaultSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; const $ = DOM.$; @@ -37,7 +38,7 @@ export const modifiedItemForeground = registerColor('settings.modifiedItemForegr registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const modifiedItemForegroundColor = theme.getColor(modifiedItemForeground); if (modifiedItemForegroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.is-configured .setting-item-title { color: ${modifiedItemForegroundColor}; }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.is-configured .setting-item-is-configured-label { color: ${modifiedItemForegroundColor}; }`); } }); @@ -47,8 +48,7 @@ export interface ITreeItem { export enum TreeItemType { setting, - groupTitle, - buttonRow + groupTitle } export interface ISettingElement extends ITreeItem { @@ -73,15 +73,20 @@ export interface IGroupElement extends ITreeItem { index: number; } -const ALL_SETTINGS_BUTTON_ID = 'all_settings_button_row'; -export interface IButtonElement extends ITreeItem { - type: TreeItemType.buttonRow; - parent: DefaultSettingsEditorModel; -} - -export type TreeElement = ISettingElement | IGroupElement | IButtonElement; +export type TreeElement = ISettingElement | IGroupElement; export type TreeElementOrRoot = TreeElement | DefaultSettingsEditorModel | SearchResultModel; +function inspectSetting(key: string, target: SettingsTarget, configurationService: IConfigurationService): { isConfigured: boolean, inspected: any, targetSelector: string } { + const inspectOverrides = URI.isUri(target) ? { resource: target } : undefined; + const inspected = configurationService.inspect(key, inspectOverrides); + const targetSelector = target === ConfigurationTarget.USER ? 'user' : + target === ConfigurationTarget.WORKSPACE ? 'workspace' : + 'workspaceFolder'; + const isConfigured = typeof inspected[targetSelector] !== 'undefined'; + + return { isConfigured, inspected, targetSelector }; +} + export class SettingsDataSource implements IDataSource { constructor( private viewState: ISettingsEditorViewState, @@ -98,9 +103,8 @@ export class SettingsDataSource implements IDataSource { } getSettingElement(setting: ISetting, group: ISettingsGroup): ISettingElement { - const targetSelector = this.viewState.settingsTarget === ConfigurationTarget.USER ? 'user' : 'workspace'; - const inspected = this.configurationService.inspect(setting.key); - const isConfigured = typeof inspected[targetSelector] !== 'undefined'; + const { isConfigured, inspected, targetSelector } = inspectSetting(setting.key, this.viewState.settingsTarget, this.configurationService); + const displayValue = isConfigured ? inspected[targetSelector] : inspected.default; const overriddenScopeList = []; if (targetSelector === 'user' && typeof inspected.workspace !== 'undefined') { @@ -169,16 +173,8 @@ export class SettingsDataSource implements IDataSource { } private getRootChildren(root: DefaultSettingsEditorModel): TreeElement[] { - const groupItems: TreeElement[] = root.settingsGroups + return root.settingsGroups .map((g, i) => this.getGroupElement(g, i)); - - groupItems.splice(1, 0, { - id: ALL_SETTINGS_BUTTON_ID, - type: TreeItemType.buttonRow, - parent: root - }); - - return groupItems; } private getGroupChildren(group: ISettingsGroup): ISettingElement[] { @@ -224,7 +220,6 @@ export function settingKeyToDisplayFormat(key: string): { category: string, labe export interface ISettingsEditorViewState { settingsTarget: SettingsTarget; showConfiguredOnly?: boolean; - showAllSettings?: boolean; } export interface IDisposableTemplate { @@ -241,7 +236,8 @@ export interface ISettingItemTemplate extends IDisposableTemplate { descriptionElement: HTMLElement; expandIndicatorElement: HTMLElement; valueElement: HTMLElement; - overridesElement: HTMLElement; + isConfiguredElement: HTMLElement; + otherOverridesElement: HTMLElement; } export interface IGroupTitleTemplate extends IDisposableTemplate { @@ -250,16 +246,8 @@ export interface IGroupTitleTemplate extends IDisposableTemplate { labelElement: HTMLElement; } -export interface IButtonRowTemplate extends IDisposableTemplate { - parent: HTMLElement; - - button: Button; - entry?: IButtonElement; -} - const SETTINGS_ELEMENT_TEMPLATE_ID = 'settings.entry.template'; const SETTINGS_GROUP_ELEMENT_TEMPLATE_ID = 'settings.group.template'; -const BUTTON_ROW_ELEMENT_TEMPLATE = 'settings.buttonRow.template'; export interface ISettingChangeEvent { key: string; @@ -270,9 +258,6 @@ export class SettingsRenderer implements IRenderer { private static readonly SETTING_ROW_HEIGHT = 75; - private readonly _onDidClickButton: Emitter = new Emitter(); - public readonly onDidClickButton: Event = this._onDidClickButton.event; - private readonly _onDidChangeSetting: Emitter = new Emitter(); public readonly onDidChangeSetting: Event = this._onDidChangeSetting.event; @@ -282,7 +267,6 @@ export class SettingsRenderer implements IRenderer { private measureContainer: HTMLElement; constructor( - private viewState: ISettingsEditorViewState, _measureContainer: HTMLElement, @IThemeService private themeService: IThemeService, @IContextViewService private contextViewService: IContextViewService @@ -304,10 +288,6 @@ export class SettingsRenderer implements IRenderer { } } - if (element.type === TreeItemType.buttonRow) { - return 60; - } - return 0; } @@ -327,10 +307,6 @@ export class SettingsRenderer implements IRenderer { return SETTINGS_GROUP_ELEMENT_TEMPLATE_ID; } - if (element.type === TreeItemType.buttonRow) { - return BUTTON_ROW_ELEMENT_TEMPLATE; - } - if (element.type === TreeItemType.setting) { return SETTINGS_ELEMENT_TEMPLATE_ID; } @@ -343,10 +319,6 @@ export class SettingsRenderer implements IRenderer { return this.renderGroupTitleTemplate(container); } - if (templateId === BUTTON_ROW_ELEMENT_TEMPLATE) { - return this.renderButtonRowTemplate(container); - } - if (templateId === SETTINGS_ELEMENT_TEMPLATE_ID) { return this.renderSettingTemplate(container); } @@ -369,26 +341,6 @@ export class SettingsRenderer implements IRenderer { return template; } - private renderButtonRowTemplate(container: HTMLElement): IButtonRowTemplate { - DOM.addClass(container, 'all-settings'); - - const buttonElement = DOM.append(container, $('.all-settings-button')); - - const button = new Button(buttonElement); - const toDispose: IDisposable[] = [button]; - - const template: IButtonRowTemplate = { - parent: container, - toDispose, - - button - }; - template.toDispose.push(attachButtonStyler(button, this.themeService)); - template.toDispose.push(button.onDidClick(e => this._onDidClickButton.fire(template.entry && template.entry.id))); - - return template; - } - private renderSettingTemplate(container: HTMLElement): ISettingItemTemplate { DOM.addClass(container, 'setting-item'); @@ -398,7 +350,8 @@ export class SettingsRenderer implements IRenderer { const titleElement = DOM.append(leftElement, $('.setting-item-title')); const categoryElement = DOM.append(titleElement, $('span.setting-item-category')); const labelElement = DOM.append(titleElement, $('span.setting-item-label')); - const overridesElement = DOM.append(titleElement, $('span.setting-item-overrides')); + const isConfiguredElement = DOM.append(titleElement, $('span.setting-item-is-configured-label')); + const otherOverridesElement = DOM.append(titleElement, $('span.setting-item-overrides')); const descriptionElement = DOM.append(leftElement, $('.setting-item-description')); const expandIndicatorElement = DOM.append(leftElement, $('.expand-indicator')); @@ -415,7 +368,8 @@ export class SettingsRenderer implements IRenderer { descriptionElement, expandIndicatorElement, valueElement, - overridesElement + isConfiguredElement, + otherOverridesElement }; // Prevent clicks from being handled by list @@ -433,10 +387,6 @@ export class SettingsRenderer implements IRenderer { (template).labelElement.textContent = (element).group.title; return; } - - if (templateId === BUTTON_ROW_ELEMENT_TEMPLATE) { - return this.renderButtonRowElement(element, template); - } } private elementIsSelected(tree: ITree, element: TreeElement): boolean { @@ -479,13 +429,16 @@ export class SettingsRenderer implements IRenderer { this.renderValue(element, isSelected, template); const resetButton = new Button(template.valueElement); - resetButton.element.title = localize('resetButtonTitle', "Reset"); + const resetText = localize('resetButtonTitle', "reset"); + resetButton.label = resetText; + resetButton.element.title = resetText; resetButton.element.classList.add('setting-reset-button'); resetButton.element.tabIndex = isSelected ? 0 : -1; attachButtonStyler(resetButton, this.themeService, { buttonBackground: Color.transparent.toString(), - buttonHoverBackground: Color.transparent.toString() + buttonHoverBackground: Color.transparent.toString(), + buttonForeground: editorActiveLinkForeground }); template.toDispose.push(resetButton.onDidClick(e => { @@ -493,14 +446,15 @@ export class SettingsRenderer implements IRenderer { })); template.toDispose.push(resetButton); - const alsoConfiguredInLabel = localize('alsoConfiguredIn', "Also modified in:"); - let overridesElementText = element.isConfigured ? 'Modified ' : ''; + template.isConfiguredElement.textContent = element.isConfigured ? localize('configured', "Modified") : ''; if (element.overriddenScopeList.length) { - overridesElementText = overridesElementText + `(${alsoConfiguredInLabel} ${element.overriddenScopeList.join(', ')})`; - } + let otherOverridesLabel = element.isConfigured ? + localize('alsoConfiguredIn', "Also modified in") : + localize('configuredIn', "Modified in"); - template.overridesElement.textContent = overridesElementText; + template.otherOverridesElement.textContent = `(${otherOverridesLabel}: ${element.overriddenScopeList.join(', ')})`; + } } private renderValue(element: ISettingElement, isSelected: boolean, template: ISettingItemTemplate): void { @@ -568,31 +522,41 @@ export class SettingsRenderer implements IRenderer { })); } - private renderButtonRowElement(element: IButtonElement, template: IButtonRowTemplate): void { - template.button.label = this.viewState.showAllSettings ? - localize('showFewerSettings', "Show Fewer Settings") : - localize('showAllSettings', "Show All Settings"); - } - disposeTemplate(tree: ITree, templateId: string, template: IDisposableTemplate): void { dispose(template.toDispose); } } export class SettingsTreeFilter implements IFilter { - constructor(private viewState: ISettingsEditorViewState) { } + constructor( + private viewState: ISettingsEditorViewState, + @IConfigurationService private configurationService: IConfigurationService + ) { } isVisible(tree: ITree, element: TreeElement): boolean { if (this.viewState.showConfiguredOnly && element.type === TreeItemType.setting) { return element.isConfigured; } - if (!this.viewState.showAllSettings && element.type === TreeItemType.groupTitle) { - return element.index === 0; + if (element.type === TreeItemType.groupTitle && this.viewState.showConfiguredOnly) { + return this.groupHasConfiguredSetting(element.group); } return true; } + + private groupHasConfiguredSetting(group: ISettingsGroup): boolean { + for (let section of group.sections) { + for (let setting of section.settings) { + const { isConfigured } = inspectSetting(setting.key, this.viewState.settingsTarget, this.configurationService); + if (isConfigured) { + return true; + } + } + } + + return false; + } } export class SettingsTreeController extends WorkbenchTreeController { @@ -617,10 +581,6 @@ export class SettingsAccessibilityProvider implements IAccessibilityProvider { return localize('groupRowAriaLabel', "{0}, group", element.group.title); } - if (element.type === TreeItemType.buttonRow) { - return localize('buttonRowAriaLabel', "{0}, button", element.id); - } - return ''; } } diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts b/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts index 8ff86fd21877813b037fce21478ce88f9efa4fd7..9c5cbf86e1893a92230120bd8e2e62669aa13cb3 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts @@ -192,7 +192,7 @@ const registry = Registry.as(Extensions.WorkbenchActio 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), '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(OpenSettings2Action, OpenSettings2Action.ID, OpenSettings2Action.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.US_COMMA }), 'Preferences: Open Settings (Preview)', 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/terminal/electron-browser/terminalConfigHelper.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts index f74ed02cf9cea712b5969769f0870596433d57ab..efbdc7e94fea1d276336c7903302009fe6c8ae29 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts @@ -48,22 +48,48 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { this.config = this._configurationService.getValue(TERMINAL_CONFIG_SECTION); } - private _measureFont(fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number): ITerminalFont { + public configFontIsMonospace(): boolean { + this._createCharMeasureElementIfNecessary(); + let fontSize = 15; + let fontFamily = this.config.fontFamily || this._configurationService.getValue('editor').fontFamily; + let i_rect = this._getBoundingRectFor('i', fontFamily, fontSize); + let w_rect = this._getBoundingRectFor('w', fontFamily, fontSize); + + let invalidBounds = !i_rect.width || !w_rect.width; + if (invalidBounds) { + // There is no reason to believe the font is not Monospace. + return true; + } + + return i_rect.width === w_rect.width; + } + + private _createCharMeasureElementIfNecessary() { // Create charMeasureElement if it hasn't been created or if it was orphaned by its parent if (!this._charMeasureElement || !this._charMeasureElement.parentElement) { this._charMeasureElement = document.createElement('div'); this.panelContainer.appendChild(this._charMeasureElement); } + } + private _getBoundingRectFor(char: string, fontFamily: string, fontSize: number): ClientRect | DOMRect { const style = this._charMeasureElement.style; - style.display = 'block'; + style.display = 'inline-block'; style.fontFamily = fontFamily; style.fontSize = fontSize + 'px'; style.lineHeight = 'normal'; - this._charMeasureElement.innerText = 'X'; + this._charMeasureElement.innerText = char; const rect = this._charMeasureElement.getBoundingClientRect(); style.display = 'none'; + return rect; + } + + private _measureFont(fontFamily: string, fontSize: number, letterSpacing: number, lineHeight: number): ITerminalFont { + this._createCharMeasureElementIfNecessary(); + + let rect = this._getBoundingRectFor('X', fontFamily, fontSize); + // Bounding client rect was invalid, use last font measurement if available. if (this._lastFontMeasurement && !rect.width && !rect.height) { return this._lastFontMeasurement; diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index 6bcd8d7dd2ede343362890ec6eac04d1e936c241..a0ffa0e0c216e9593b851e60873fc948f8094dac 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -65,6 +65,7 @@ export class TerminalInstance implements ITerminalInstance { private _rows: number; private _windowsShellHelper: WindowsShellHelper; private _onLineDataListeners: ((lineData: string) => void)[]; + private _onDataListeners: ((data: string) => void)[]; private _xtermReadyPromise: TPromise; private _disposables: lifecycle.IDisposable[]; @@ -119,6 +120,7 @@ export class TerminalInstance implements ITerminalInstance { this._disposables = []; this._skipTerminalCommands = []; this._onLineDataListeners = []; + this._onDataListeners = []; this._isExiting = false; this._hadFocusOnExit = false; this._isVisible = false; @@ -279,7 +281,7 @@ export class TerminalInstance implements ITerminalInstance { this._xterm.winptyCompatInit(); this._xterm.on('linefeed', () => this._onLineFeed()); if (this._processManager) { - this._processManager.onProcessData(data => this._sendPtyDataToXterm(data)); + this._processManager.onProcessData(data => this._onProcessData(data)); this._xterm.on('data', data => this._processManager.write(data)); // TODO: How does the cwd work on detached processes? this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._xterm, platform.platform, this._processManager.initialCwd); @@ -675,13 +677,20 @@ export class TerminalInstance implements ITerminalInstance { } } - private _sendPtyDataToXterm(data: string): void { + private _onProcessData(data: string): void { if (this._widgetManager) { this._widgetManager.closeMessage(); } if (this._xterm) { this._xterm.write(data); } + this._onDataListeners.forEach(listener => { + try { + listener(data); + } catch (err) { + console.error(`onData listener threw`, err); + } + }); } private _onProcessExit(exitCode: number): void { @@ -775,7 +784,7 @@ export class TerminalInstance implements ITerminalInstance { if (oldTitle !== this._title) { this.setTitle(this._title, true); } - this._processManager.onProcessData(data => this._sendPtyDataToXterm(data)); + this._processManager.onProcessData(data => this._onProcessData(data)); // Clean up waitOnExit state if (this._isExiting && this._shellLaunchConfig.waitOnExit) { @@ -788,7 +797,15 @@ export class TerminalInstance implements ITerminalInstance { } public onData(listener: (data: string) => void): lifecycle.IDisposable { - return this._processManager.onProcessData(data => listener(data)); + this._onDataListeners.push(listener); + return { + dispose: () => { + const i = this._onDataListeners.indexOf(listener); + if (i >= 0) { + this._onDataListeners.splice(i, 1); + } + } + }; } public onLineData(listener: (lineData: string) => void): lifecycle.IDisposable { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts index d07fca022b7e93ce8c2319163c0255c07336d032..7b194643901c5cd54ca153cfb6767be91e1ca955 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts @@ -26,6 +26,9 @@ import { PANEL_BACKGROUND, PANEL_BORDER } from 'vs/workbench/common/theme'; import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR } from 'vs/workbench/parts/terminal/common/terminalColorRegistry'; import { DataTransfers } from 'vs/base/browser/dnd'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { INotificationService, IPromptChoice } from 'vs/platform/notification/common/notification'; +import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper'; +import { Severity } from 'vs/editor/editor.api'; export class TerminalPanel extends Panel { @@ -45,7 +48,8 @@ export class TerminalPanel extends Panel { @ITerminalService private readonly _terminalService: ITerminalService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IThemeService protected themeService: IThemeService, - @ITelemetryService telemetryService: ITelemetryService + @ITelemetryService telemetryService: ITelemetryService, + @INotificationService private readonly _notificationService: INotificationService ) { super(TERMINAL_PANEL_ID, telemetryService, themeService); } @@ -74,6 +78,19 @@ export class TerminalPanel extends Panel { if (e.affectsConfiguration('terminal.integrated') || e.affectsConfiguration('editor.fontFamily')) { this._updateFont(); } + + if (e.affectsConfiguration('terminal.integrated.fontFamily') || e.affectsConfiguration('editor.fontFamily')) { + let configHelper = this._terminalService.configHelper; + if (configHelper instanceof TerminalConfigHelper) { + if (!configHelper.configFontIsMonospace()) { + const choices: IPromptChoice[] = [{ + label: nls.localize('terminal.useMonospace', "Use 'monospace'"), + run: () => this._configurationService.updateValue('terminal.integrated.fontFamily', 'monospace'), + }]; + this._notificationService.prompt(Severity.Warning, nls.localize('terminal.monospaceOnly', "The terminal only supports monospace fonts."), choices); + } + } + } })); this._updateFont(); this._updateTheme(); diff --git a/src/vs/workbench/parts/terminal/test/electron-browser/terminalConfigHelper.test.ts b/src/vs/workbench/parts/terminal/test/electron-browser/terminalConfigHelper.test.ts index cc4cec690c45ca1ed66b6ba137b98ddcfda5d046..27cc89f26cc653bbb099e6fb1795f5e59b7912a3 100644 --- a/src/vs/workbench/parts/terminal/test/electron-browser/terminalConfigHelper.test.ts +++ b/src/vs/workbench/parts/terminal/test/electron-browser/terminalConfigHelper.test.ts @@ -125,4 +125,89 @@ suite('Workbench - TerminalConfigHelper', () => { configHelper.panelContainer = fixture; assert.equal(configHelper.getFont().lineHeight, 1, 'editor.lineHeight should be 1 when terminal.integrated.lineHeight not set'); }); + + test('TerminalConfigHelper - isMonospace monospace', function () { + const configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration('terminal', { + integrated: { + fontFamily: 'monospace' + } + }); + + let configHelper = new TerminalConfigHelper(configurationService, null, null, null); + configHelper.panelContainer = fixture; + assert.equal(configHelper.configFontIsMonospace(), true, 'monospace is monospaced'); + }); + + test('TerminalConfigHelper - isMonospace sans-serif', function () { + const configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration('terminal', { + integrated: { + fontFamily: 'sans-serif' + } + }); + let configHelper = new TerminalConfigHelper(configurationService, null, null, null); + configHelper.panelContainer = fixture; + assert.equal(configHelper.configFontIsMonospace(), false, 'sans-serif is not monospaced'); + }); + + test('TerminalConfigHelper - isMonospace serif', function () { + const configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration('terminal', { + integrated: { + fontFamily: 'serif' + } + }); + let configHelper = new TerminalConfigHelper(configurationService, null, null, null); + configHelper.panelContainer = fixture; + assert.equal(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); + }); + + test('TerminalConfigHelper - isMonospace monospace falls back to editor.fontFamily', function () { + const configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration('editor', { + fontFamily: 'monospace' + }); + configurationService.setUserConfiguration('terminal', { + integrated: { + fontFamily: null + } + }); + + let configHelper = new TerminalConfigHelper(configurationService, null, null, null); + configHelper.panelContainer = fixture; + assert.equal(configHelper.configFontIsMonospace(), true, 'monospace is monospaced'); + }); + + test('TerminalConfigHelper - isMonospace sans-serif falls back to editor.fontFamily', function () { + const configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration('editor', { + fontFamily: 'sans-serif' + }); + configurationService.setUserConfiguration('terminal', { + integrated: { + fontFamily: null + } + }); + + let configHelper = new TerminalConfigHelper(configurationService, null, null, null); + configHelper.panelContainer = fixture; + assert.equal(configHelper.configFontIsMonospace(), false, 'sans-serif is not monospaced'); + }); + + test('TerminalConfigHelper - isMonospace serif falls back to editor.fontFamily', function () { + const configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration('editor', { + fontFamily: 'serif' + }); + configurationService.setUserConfiguration('terminal', { + integrated: { + fontFamily: null + } + }); + + let configHelper = new TerminalConfigHelper(configurationService, null, null, null); + configHelper.panelContainer = fixture; + assert.equal(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); + }); }); \ No newline at end of file diff --git a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts index 930ed10fd4c9ef7359c954f6b7f839e05518107a..6fb507a06561bc4dff6a768223b0dfd63554bb2d 100644 --- a/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts +++ b/src/vs/workbench/services/preferences/common/preferencesEditorInput.ts @@ -98,7 +98,7 @@ export class SettingsEditor2Input extends EditorInput { } getName(): string { - return nls.localize('settingsEditor2InputName', "Settings (Experimental)"); + return nls.localize('settingsEditor2InputName', "Settings (Preview)"); } resolve(refresh?: boolean): TPromise { diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index ef0ea1b5654a2c108bb54423aa6ed41eed61551b..d3efcf7e8b05cbb9c05d167c0425b0473b4a6633 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -121,17 +121,20 @@ export class SearchService implements ISearchService { // TODO@roblou this is not properly waiting for search-rg to finish registering itself if (this.searchProvider.length) { return TPromise.join(this.searchProvider.map(p => searchWithProvider(p))) - .then(complete => { - const first: ISearchComplete = complete[0]; - if (!first) { + .then(completes => { + completes = completes.filter(c => !!c); + if (!completes.length) { return null; } return { - limitHit: first && first.limitHit, - stats: first.stats, - results: arrays.flatten(complete.map(c => c.results)) + limitHit: completes[0] && completes[0].limitHit, + stats: completes[0].stats, + results: arrays.flatten(completes.map(c => c.results)) }; + }, errs => { + errs = errs.filter(e => !!e); + return TPromise.wrapError(errs[0]); }); } else { return searchWithProvider(this.diskSearch); 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 2bc337d218f791770f9084b0880cfa340311d72d..b831fbd49f68027f682dd2fb019546d71a1d829b 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -17,6 +17,7 @@ import * as vscode from 'vscode'; import { dispose } from 'vs/base/common/lifecycle'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { Range } from 'vs/workbench/api/node/extHostTypes'; +import { joinPath } from 'vs/base/common/resources'; let rpcProtocol: TestRPCProtocol; let extHostSearch: ExtHostSearch; @@ -53,8 +54,8 @@ class MockMainThreadSearch implements MainThreadSearchShape { let mockExtfs: Partial; suite('ExtHostSearch', () => { - async function registerTestSearchProvider(provider: vscode.SearchProvider): TPromise { - disposables.push(extHostSearch.registerSearchProvider('file', provider)); + async function registerTestSearchProvider(provider: vscode.SearchProvider, scheme = 'file'): TPromise { + disposables.push(extHostSearch.registerSearchProvider(scheme, provider)); await rpcProtocol.sync(); } @@ -121,12 +122,8 @@ suite('ExtHostSearch', () => { const rootFolderA = URI.file('/foo/bar1'); const rootFolderB = URI.file('/foo/bar2'); - // const rootFolderC = URI.file('/foo/bar3'); - - function makeAbsoluteURI(root: URI, relativePath: string): URI { - return URI.file( - path.join(root.fsPath, relativePath)); - } + const fancyScheme = 'fancy'; + const fancySchemeFolderA = URI.from({ scheme: fancyScheme, path: '/project/folder1' }); suite('File:', () => { @@ -162,9 +159,9 @@ suite('ExtHostSearch', () => { test('simple results', async () => { const reportedResults = [ - makeAbsoluteURI(rootFolderA, 'file1.ts'), - makeAbsoluteURI(rootFolderA, 'file2.ts'), - makeAbsoluteURI(rootFolderA, 'file3.ts') + joinPath(rootFolderA, 'file1.ts'), + joinPath(rootFolderA, 'file2.ts'), + joinPath(rootFolderA, 'file3.ts') ]; await registerTestSearchProvider({ @@ -383,7 +380,7 @@ suite('ExtHostSearch', () => { compareURIs( results, [ - makeAbsoluteURI(rootFolderA, 'file1.ts') + joinPath(rootFolderA, 'file1.ts') ]); }); @@ -443,20 +440,20 @@ suite('ExtHostSearch', () => { compareURIs( results, [ - makeAbsoluteURI(rootFolderA, 'folder/fileA.scss'), - makeAbsoluteURI(rootFolderA, 'folder/file2.css'), + joinPath(rootFolderA, 'folder/fileA.scss'), + joinPath(rootFolderA, 'folder/file2.css'), - makeAbsoluteURI(rootFolderB, 'fileB.ts'), - makeAbsoluteURI(rootFolderB, 'fileB.js'), - makeAbsoluteURI(rootFolderB, 'file3.js'), + joinPath(rootFolderB, 'fileB.ts'), + joinPath(rootFolderB, 'fileB.js'), + joinPath(rootFolderB, 'file3.js'), ]); }); test('max results = 1', async () => { const reportedResults = [ - makeAbsoluteURI(rootFolderA, 'file1.ts'), - makeAbsoluteURI(rootFolderA, 'file2.ts'), - makeAbsoluteURI(rootFolderA, 'file3.ts'), + joinPath(rootFolderA, 'file1.ts'), + joinPath(rootFolderA, 'file2.ts'), + joinPath(rootFolderA, 'file3.ts'), ]; let wasCanceled = false; @@ -490,9 +487,9 @@ suite('ExtHostSearch', () => { test('max results = 2', async () => { const reportedResults = [ - makeAbsoluteURI(rootFolderA, 'file1.ts'), - makeAbsoluteURI(rootFolderA, 'file2.ts'), - makeAbsoluteURI(rootFolderA, 'file3.ts'), + joinPath(rootFolderA, 'file1.ts'), + joinPath(rootFolderA, 'file2.ts'), + joinPath(rootFolderA, 'file3.ts'), ]; let wasCanceled = false; @@ -567,9 +564,9 @@ suite('ExtHostSearch', () => { test('respects filePattern', async () => { const reportedResults = [ - makeAbsoluteURI(rootFolderA, 'file1.ts'), - makeAbsoluteURI(rootFolderA, 'file2.ts'), - makeAbsoluteURI(rootFolderA, 'file3.ts'), + joinPath(rootFolderA, 'file1.ts'), + joinPath(rootFolderA, 'file2.ts'), + joinPath(rootFolderA, 'file3.ts'), ]; await registerTestSearchProvider({ @@ -596,6 +593,35 @@ suite('ExtHostSearch', () => { compareURIs(results, reportedResults.slice(2)); }); + test('works with non-file schemes', async () => { + const reportedResults = [ + joinPath(fancySchemeFolderA, 'file1.ts'), + joinPath(fancySchemeFolderA, 'file2.ts'), + joinPath(fancySchemeFolderA, 'file3.ts'), + + ]; + + await registerTestSearchProvider({ + provideFileSearchResults(options: vscode.FileSearchOptions, progress: vscode.Progress, token: vscode.CancellationToken): Thenable { + reportedResults.forEach(r => progress.report(path.basename(r.fsPath))); + return TPromise.wrap(null); + } + }, fancyScheme); + + const query: ISearchQuery = { + type: QueryType.File, + filePattern: '', + folderQueries: [ + { + folder: fancySchemeFolderA + } + ] + }; + + const results = await runFileSearch(query); + compareURIs(results, reportedResults); + }); + // Mock fs? // test('Returns result for absolute path', async () => { // const queriedFile = makeFileResult(rootFolderA, 'file2.ts'); @@ -629,7 +655,6 @@ 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), @@ -653,11 +678,11 @@ suite('ExtHostSearch', () => { }; } - function assertResults(actual: IFileMatch[], expected: vscode.TextSearchResult[]) { + function assertResults(actual: IFileMatch[], expected: vscode.TextSearchResult[], folder = rootFolderA) { const actualTextSearchResults: vscode.TextSearchResult[] = []; for (let fileMatch of actual) { // Make relative - const relativePath = fileMatch.resource.fsPath.substr(rootFolderA.fsPath.length + 1); + const relativePath = fileMatch.resource.toString().substr(folder.toString().length + 1); for (let lineMatch of fileMatch.lineMatches) { for (let [offset, length] of lineMatch.offsetAndLengths) { actualTextSearchResults.push({ @@ -884,7 +909,7 @@ suite('ExtHostSearch', () => { test('multiroot sibling clause', async () => { mockExtfs.readdir = (_path: string, callback: (error: Error, files: string[]) => void) => { - if (_path === makeAbsoluteURI(rootFolderA, 'folder').fsPath) { + if (_path === joinPath(rootFolderA, 'folder').fsPath) { callback(null, [ 'fileA.scss', 'fileA.css', @@ -1080,5 +1105,31 @@ suite('ExtHostSearch', () => { assert.equal(results.length, 2); assert.equal(cancels, 2); }); + + test('works with non-file schemes', async () => { + const providedResults: vscode.TextSearchResult[] = [ + makeTextResult('file1.ts'), + makeTextResult('file2.ts'), + makeTextResult('file3.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); + } + }, fancyScheme); + + const query: ISearchQuery = { + type: QueryType.Text, + + folderQueries: [ + { folder: fancySchemeFolderA } + ] + }; + + const results = await runTextSearch(getPattern('foo'), query); + assertResults(results, providedResults, fancySchemeFolderA); + }); }); });