提交 31694f7a 编写于 作者: B Benjamin Pasero

Merge branch 'master' into ben/editor

......@@ -23,7 +23,7 @@
"onCommand:markdown.showLockedPreviewToSide",
"onCommand:markdown.showSource",
"onCommand:markdown.showPreviewSecuritySelector",
"onView:markdown.preview"
"onWebviewPanel:markdown.preview"
],
"contributes": {
"commands": [
......
......@@ -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 = [
......
......@@ -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 `<pre class="hljs"><code><div>${this.md!.utils.escapeHtml(str)}</div></code></pre>`;
}
}).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) {
......
......@@ -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
};
......@@ -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<TocEntry | undefined> {
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,
......
......@@ -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);
}
......@@ -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);
});
});
......@@ -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<T>(items: T[], resourceAccessor: (item: T) => uri): T[] {
const distinctParents: T[] = [];
for (let i = 0; i < items.length; i++) {
......
......@@ -38,21 +38,26 @@ export function collectLaunchConfigs(folder: string): Promise<WorkspaceStatItem[
return resolve([]);
}
const json = JSON.parse(contents.toString());
if (json['configurations']) {
for (const each of json['configurations']) {
const type = each['type'];
if (type) {
if (launchConfigs.has(type)) {
launchConfigs.set(type, launchConfigs.get(type) + 1);
}
else {
launchConfigs.set(type, 1);
try {
const json = JSON.parse(contents.toString());
if (json['configurations']) {
for (const each of json['configurations']) {
const type = each['type'];
if (type) {
if (launchConfigs.has(type)) {
launchConfigs.set(type, launchConfigs.get(type) + 1);
}
else {
launchConfigs.set(type, 1);
}
}
}
}
} catch (e) {
console.log(`Unable to parse ${launchConfig}`);
}
return resolve(asSortedItems(launchConfigs));
});
} else {
......
......@@ -150,7 +150,7 @@ export class IssueReporter extends Disposable {
const content: string[] = [];
if (styles.inputBackground) {
content.push(`input[type="text"], textarea, select, .issues-container > .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) {
......
......@@ -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%;
}
......
......@@ -638,7 +638,7 @@ export class SuggestWidget implements IContentWidget, IDelegate<ICompletionItem>
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<ICompletionItem>
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<ICompletionItem>
*/
this.telemetryService.publicLog('suggestWidget', { ...stats, ...this.editor.getTelemetryData() });
this.focusedItem = null;
this.list.splice(0, this.list.length, this.completionModel.items);
if (isFrozen) {
......
......@@ -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,
......
......@@ -13,6 +13,7 @@ export interface ILocalization {
languageName?: string;
languageNameLocalized?: string;
translations: ITranslation[];
minimalTranslations?: { [key: string]: string };
}
export interface ITranslation {
......
/*---------------------------------------------------------------------------------------------
* 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')
};
......@@ -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<void>;
}
/**
* 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).
......
......@@ -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<void>;
}
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
/**
......
......@@ -137,7 +137,7 @@ export class MainThreadWebviews implements MainThreadWebviewsShape, WebviewReviv
reviveWebview(webview: WebviewEditorInput): TPromise<void> {
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);
......
......@@ -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(<vscode.TerminalOptions>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);
})
......
......@@ -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 | 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<T> {
}
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)
};
}
......
......@@ -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<void> {
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);
}
......
......@@ -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<IExtensionsConfiguration>(ConfigurationKey);
const languagePackSuggestionIgnoreList = <string[]>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);
......
......@@ -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<IWorkbenchActionRegistry>(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 = <string[]>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');
}
);
});
});
});
}
......
......@@ -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 {
......
......@@ -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,
......
......@@ -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;
......
......@@ -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, <IButtonElement>{
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<string> = new Emitter<string>();
public readonly onDidClickButton: Event<string> = this._onDidClickButton.event;
private readonly _onDidChangeSetting: Emitter<ISettingChangeEvent> = new Emitter<ISettingChangeEvent>();
public readonly onDidChangeSetting: Event<ISettingChangeEvent> = 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 {
(<IGroupTitleTemplate>template).labelElement.textContent = (<IGroupElement>element).group.title;
return;
}
if (templateId === BUTTON_ROW_ELEMENT_TEMPLATE) {
return this.renderButtonRowElement(<IButtonElement>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 '';
}
}
......
......@@ -192,7 +192,7 @@ const registry = Registry.as<IWorkbenchActionRegistry>(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);
......
......@@ -48,22 +48,48 @@ export class TerminalConfigHelper implements ITerminalConfigHelper {
this.config = this._configurationService.getValue<ITerminalConfiguration>(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<IEditorOptions>('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;
......
......@@ -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<void>;
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 {
......
......@@ -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();
......
......@@ -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
......@@ -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<DefaultSettingsEditorModel> {
......
......@@ -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 <ISearchComplete>{
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);
......
......@@ -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<typeof extfs>;
suite('ExtHostSearch', () => {
async function registerTestSearchProvider(provider: vscode.SearchProvider): TPromise<void> {
disposables.push(extHostSearch.registerSearchProvider('file', provider));
async function registerTestSearchProvider(provider: vscode.SearchProvider, scheme = 'file'): TPromise<void> {
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<string>, token: vscode.CancellationToken): Thenable<void> {
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<vscode.TextSearchResult>, token: vscode.CancellationToken): Thenable<void> {
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);
});
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册