diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 192c87c4bf1bb9e8442bc847a9cbaa87c8429eca..764e32a0c845a5bb2a943f9730b8428ace2e454f 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -19,8 +19,9 @@ export interface IIconLabelCreationOptions { supportDescriptionHighlights?: boolean; } -export interface IIconLabelOptions { +export interface IIconLabelValueOptions { title?: string; + descriptionTitle?: string; extraClasses?: string[]; italic?: boolean; matches?: IMatch[]; @@ -121,7 +122,7 @@ export class IconLabel { ]); } - public setValue(label?: string, description?: string, options?: IIconLabelOptions): void { + public setValue(label?: string, description?: string, options?: IIconLabelValueOptions): void { const classes = ['monaco-icon-label']; if (options) { if (options.extraClasses) { @@ -149,8 +150,14 @@ export class IconLabel { if (this.descriptionNode instanceof HighlightedLabel) { this.descriptionNode.set(description || '', options ? options.descriptionMatches : void 0); + if (options && options.descriptionTitle) { + this.descriptionNode.element.title = options.descriptionTitle; + } else { + this.descriptionNode.element.removeAttribute('title'); + } } else { this.descriptionNode.textContent = description || ''; + this.descriptionNode.title = options && options.descriptionTitle ? options.descriptionTitle : ''; this.descriptionNode.empty = !description; } } diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index df7c0b99ba6bc529a9b973052d6858b98fc96994..47f33833fe286c9755c11c7fd33e5b3ffc4f1e29 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -160,8 +160,7 @@ export class InputBox extends Widget { } if (this.placeholder) { - this.input.setAttribute('placeholder', this.placeholder); - this.input.title = this.placeholder; + this.setPlaceHolder(this.placeholder); } this.oninput(this.input, () => this.onValueChange()); @@ -204,6 +203,7 @@ export class InputBox extends Widget { public setPlaceHolder(placeHolder: string): void { if (this.input) { this.input.setAttribute('placeholder', placeHolder); + this.input.title = placeHolder; } } diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index 1f4df3e671cafd242f5b33af4ccafc130273431d..d5bec85ebafeece68f090d59c783086c8319eb10 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -7,7 +7,6 @@ import strings = require('vs/base/common/strings'); import { LRUCache } from 'vs/base/common/map'; import { CharCode } from 'vs/base/common/charCode'; -import { ltrim } from 'vs/base/common/strings'; export interface IFilter { // Returns null if word doesn't match. @@ -342,108 +341,6 @@ export function matchesFuzzy(word: string, wordToMatchAgainst: string, enableSep return enableSeparateSubstringMatching ? fuzzySeparateFilter(word, wordToMatchAgainst) : fuzzyContiguousFilter(word, wordToMatchAgainst); } -const octiconStartMarker = '$('; - -export function matchesFuzzyOcticonAware(word: string, wordToMatchAgainst: string, enableSeparateSubstringMatching = false): IMatch[] { - - // Return early if there are no octicon markers in the word to match against - const firstOcticonIndex = wordToMatchAgainst.indexOf(octiconStartMarker); - if (firstOcticonIndex === -1) { - return matchesFuzzy(word, wordToMatchAgainst, enableSeparateSubstringMatching); - } - - const octiconOffsets: number[] = []; - - let wordToMatchAgainstWithoutOcticons: string = ''; - - function appendChars(chars: string) { - if (chars) { - wordToMatchAgainstWithoutOcticons += chars; - - for (let i = 0; i < chars.length; i++) { - octiconOffsets.push(octiconsOffset); // make sure to fill in octicon offsets - } - } - } - - let currentOcticonStart = -1; - let currentOcticonValue: string = ''; - let octiconsOffset = 0; - - let char: string; - let nextChar: string; - - let offset = firstOcticonIndex; - const length = wordToMatchAgainst.length; - - // Append all characters until the first octicon - appendChars(wordToMatchAgainst.substr(0, firstOcticonIndex)); - - // example: $(file-symlink-file) my cool $(other-octicon) entry - while (offset < length) { - char = wordToMatchAgainst[offset]; - nextChar = wordToMatchAgainst[offset + 1]; - - // beginning of octicon: some value $( <-- - if (char === octiconStartMarker[0] && nextChar === octiconStartMarker[1]) { - currentOcticonStart = offset; - - // if we had a previous potential octicon value without - // the closing ')', it was actually not an octicon and - // so we have to add it to the actual value - appendChars(currentOcticonValue); - - currentOcticonValue = octiconStartMarker; - - offset++; // jump over '(' - } - - // end of octicon: some value $(some-octicon) <-- - else if (char === ')' && currentOcticonStart !== -1) { - const currentOcticonLength = offset - currentOcticonStart + 1; // +1 to include the closing ')' - octiconsOffset += currentOcticonLength; - currentOcticonStart = -1; - currentOcticonValue = ''; - } - - // within octicon - else if (currentOcticonStart !== -1) { - currentOcticonValue += char; - } - - // any value outside of octicons - else { - appendChars(char); - } - - offset++; - } - - // if we had a previous potential octicon value without - // the closing ')', it was actually not an octicon and - // so we have to add it to the actual value - appendChars(currentOcticonValue); - - // Trim the word to match against because it could have leading - // whitespace now if the word started with an octicon - const wordToMatchAgainstWithoutOcticonsTrimmed = ltrim(wordToMatchAgainstWithoutOcticons, ' '); - const leadingWhitespaceOffset = wordToMatchAgainstWithoutOcticons.length - wordToMatchAgainstWithoutOcticonsTrimmed.length; - - // match on value without octicons - const matches = matchesFuzzy(word, wordToMatchAgainstWithoutOcticonsTrimmed, enableSeparateSubstringMatching); - - // Map matches back to offsets with octicons and trimming - if (matches) { - for (let i = 0; i < matches.length; i++) { - const octiconOffset = octiconOffsets[matches[i].start] /* octicon offsets at index */ + leadingWhitespaceOffset /* overall leading whitespace offset */; - matches[i].start += octiconOffset; - matches[i].end += octiconOffset; - } - } - - return matches; -} - export function skipScore(pattern: string, word: string, patternMaxWhitespaceIgnore?: number): [number, number[]] { pattern = pattern.toLowerCase(); word = word.toLowerCase(); diff --git a/src/vs/base/common/octicon.ts b/src/vs/base/common/octicon.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b446e5559428ef5ab014aadcacc5f27b828b564 --- /dev/null +++ b/src/vs/base/common/octicon.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { matchesFuzzy, IMatch } from 'vs/base/common/filters'; +import { ltrim } from 'vs/base/common/strings'; + +const octiconStartMarker = '$('; + +export function removeOcticons(word: string): string { + const firstOcticonIndex = word.indexOf(octiconStartMarker); + if (firstOcticonIndex === -1) { + return word; // return early if the word does not include an octicon + } + + return doParseOcticonAware(word, firstOcticonIndex).wordWithoutOcticons.trim(); +} + +function doParseOcticonAware(word: string, firstOcticonIndex: number): { wordWithoutOcticons: string, octiconOffsets: number[] } { + const octiconOffsets: number[] = []; + let wordWithoutOcticons: string = ''; + + function appendChars(chars: string) { + if (chars) { + wordWithoutOcticons += chars; + + for (let i = 0; i < chars.length; i++) { + octiconOffsets.push(octiconsOffset); // make sure to fill in octicon offsets + } + } + } + + let currentOcticonStart = -1; + let currentOcticonValue: string = ''; + let octiconsOffset = 0; + + let char: string; + let nextChar: string; + + let offset = firstOcticonIndex; + const length = word.length; + + // Append all characters until the first octicon + appendChars(word.substr(0, firstOcticonIndex)); + + // example: $(file-symlink-file) my cool $(other-octicon) entry + while (offset < length) { + char = word[offset]; + nextChar = word[offset + 1]; + + // beginning of octicon: some value $( <-- + if (char === octiconStartMarker[0] && nextChar === octiconStartMarker[1]) { + currentOcticonStart = offset; + + // if we had a previous potential octicon value without + // the closing ')', it was actually not an octicon and + // so we have to add it to the actual value + appendChars(currentOcticonValue); + + currentOcticonValue = octiconStartMarker; + + offset++; // jump over '(' + } + + // end of octicon: some value $(some-octicon) <-- + else if (char === ')' && currentOcticonStart !== -1) { + const currentOcticonLength = offset - currentOcticonStart + 1; // +1 to include the closing ')' + octiconsOffset += currentOcticonLength; + currentOcticonStart = -1; + currentOcticonValue = ''; + } + + // within octicon + else if (currentOcticonStart !== -1) { + currentOcticonValue += char; + } + + // any value outside of octicons + else { + appendChars(char); + } + + offset++; + } + + // if we had a previous potential octicon value without + // the closing ')', it was actually not an octicon and + // so we have to add it to the actual value + appendChars(currentOcticonValue); + + return { wordWithoutOcticons, octiconOffsets }; +} + +export function matchesFuzzyOcticonAware(word: string, wordToMatchAgainst: string, enableSeparateSubstringMatching = false): IMatch[] { + + // Return early if there are no octicon markers in the word to match against + const firstOcticonIndex = wordToMatchAgainst.indexOf(octiconStartMarker); + if (firstOcticonIndex === -1) { + return matchesFuzzy(word, wordToMatchAgainst, enableSeparateSubstringMatching); + } + + // Parse + const { wordWithoutOcticons, octiconOffsets } = doParseOcticonAware(wordToMatchAgainst, firstOcticonIndex); + + // Trim the word to match against because it could have leading + // whitespace now if the word started with an octicon + const wordToMatchAgainstWithoutOcticonsTrimmed = ltrim(wordWithoutOcticons, ' '); + const leadingWhitespaceOffset = wordWithoutOcticons.length - wordToMatchAgainstWithoutOcticonsTrimmed.length; + + // match on value without octicons + const matches = matchesFuzzy(word, wordToMatchAgainstWithoutOcticonsTrimmed, enableSeparateSubstringMatching); + + // Map matches back to offsets with octicons and trimming + if (matches) { + for (let i = 0; i < matches.length; i++) { + const octiconOffset = octiconOffsets[matches[i].start] /* octicon offsets at index */ + leadingWhitespaceOffset /* overall leading whitespace offset */; + matches[i].start += octiconOffset; + matches[i].end += octiconOffset; + } + } + + return matches; +} \ No newline at end of file diff --git a/src/vs/base/parts/quickopen/browser/quickOpenModel.ts b/src/vs/base/parts/quickopen/browser/quickOpenModel.ts index 57f2b4ad041354951af45cab9e49bbc74d97009b..0880137be991c2ca5a7660defb41167e249f3e61 100644 --- a/src/vs/base/parts/quickopen/browser/quickOpenModel.ts +++ b/src/vs/base/parts/quickopen/browser/quickOpenModel.ts @@ -10,7 +10,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import types = require('vs/base/common/types'); import URI from 'vs/base/common/uri'; import { ITree, IActionProvider } from 'vs/base/parts/tree/browser/tree'; -import { IconLabel, IIconLabelOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IQuickNavigateConfiguration, IModel, IDataSource, IFilter, IAccessiblityProvider, IRenderer, IRunner, Mode } from 'vs/base/parts/quickopen/common/quickOpen'; import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; import { compareAnything } from 'vs/base/common/comparers'; @@ -84,7 +84,7 @@ export class QuickOpenEntry { /** * The options for the label to use for this entry */ - public getLabelOptions(): IIconLabelOptions { + public getLabelOptions(): IIconLabelValueOptions { return null; } @@ -123,6 +123,13 @@ export class QuickOpenEntry { return null; } + /** + * A tooltip to show when hovering over the description portion of the entry. + */ + public getDescriptionTooltip(): string { + return null; + } + /** * An optional keybinding to show for an entry. */ @@ -227,7 +234,7 @@ export class QuickOpenEntryGroup extends QuickOpenEntry { return this.entry ? this.entry.getLabel() : super.getLabel(); } - public getLabelOptions(): IIconLabelOptions { + public getLabelOptions(): IIconLabelValueOptions { return this.entry ? this.entry.getLabelOptions() : super.getLabelOptions(); } @@ -459,9 +466,10 @@ class Renderer implements IRenderer { data.icon.className = iconClass; // Label - const options: IIconLabelOptions = entry.getLabelOptions() || Object.create(null); + const options: IIconLabelValueOptions = entry.getLabelOptions() || Object.create(null); options.matches = labelHighlights || []; - options.title = entry.getTooltip() || void 0; + options.title = entry.getTooltip(); + options.descriptionTitle = entry.getDescriptionTooltip() || entry.getDescription(); // tooltip over description because it could overflow options.descriptionMatches = descriptionHighlights || []; data.label.setValue(entry.getLabel(), entry.getDescription(), options); diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index d8a52cbb2ef5eedff5d4b63512ab1be4975f2b64..390502547557e049a371166f841660477760ed25 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -5,7 +5,8 @@ 'use strict'; import * as assert from 'assert'; -import { IFilter, or, matchesPrefix, matchesStrictPrefix, matchesCamelCase, matchesSubString, matchesContiguousSubString, matchesWords, fuzzyScore, IMatch, fuzzyScoreGraceful, fuzzyScoreGracefulAggressive, matchesFuzzy, matchesFuzzyOcticonAware } from 'vs/base/common/filters'; +import { IFilter, or, matchesPrefix, matchesStrictPrefix, matchesCamelCase, matchesSubString, matchesContiguousSubString, matchesWords, fuzzyScore, IMatch, fuzzyScoreGraceful, fuzzyScoreGracefulAggressive, matchesFuzzy } from 'vs/base/common/filters'; +import { matchesFuzzyOcticonAware } from 'vs/base/common/octicon'; function filterOk(filter: IFilter, word: string, wordToMatchAgainst: string, highlights?: { start: number; end: number; }[]) { let r = filter(word, wordToMatchAgainst); diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 2ced1db3e2f407c9602e4caa0ab2d66dccbc126a..9a244c0f95f3c137d6fb05d69c7c341ea74a877e 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -7,7 +7,7 @@ import uri from 'vs/base/common/uri'; import resources = require('vs/base/common/resources'); -import { IconLabel, IIconLabelOptions, IIconLabelCreationOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IconLabel, IIconLabelValueOptions, IIconLabelCreationOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IEditorInput } from 'vs/platform/editor/common/editor'; @@ -32,7 +32,7 @@ export interface IResourceLabel { resource?: uri; } -export interface IResourceLabelOptions extends IIconLabelOptions { +export interface IResourceLabelOptions extends IIconLabelValueOptions { fileKind?: FileKind; fileDecorations?: { colors: boolean, badges: boolean, data?: IDecorationData }; } @@ -172,7 +172,7 @@ export class ResourceLabel extends IconLabel { return; } - const iconLabelOptions: IIconLabelOptions = { + const iconLabelOptions: IIconLabelValueOptions = { title: '', italic: this.options && this.options.italic, matches: this.options && this.options.matches, diff --git a/src/vs/workbench/browser/parts/editor/editorPicker.ts b/src/vs/workbench/browser/parts/editor/editorPicker.ts index 979df653a597eb3f52c70566d1432a9cfc6dac05..cea42aceaa9a3ef285a1775eca5b79f31ec28341 100644 --- a/src/vs/workbench/browser/parts/editor/editorPicker.ts +++ b/src/vs/workbench/browser/parts/editor/editorPicker.ts @@ -9,7 +9,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import nls = require('vs/nls'); import URI from 'vs/base/common/uri'; import errors = require('vs/base/common/errors'); -import { IIconLabelOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IAutoFocus, Mode, IEntryRunContext, IQuickNavigateConfiguration, IModel } from 'vs/base/parts/quickopen/common/quickOpen'; import { QuickOpenModel, QuickOpenEntry, QuickOpenEntryGroup, QuickOpenItemAccessor } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -39,7 +39,7 @@ export class EditorPickerEntry extends QuickOpenEntryGroup { this.stacks = editorGroupService.getStacksModel(); } - public getLabelOptions(): IIconLabelOptions { + public getLabelOptions(): IIconLabelValueOptions { return { extraClasses: getIconClasses(this.modelService, this.modeService, this.getResource()), italic: this._group.isPreview(this.editor) diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index e910e1ad7f5532f9c8e95354339e4395310cf61a..9d3f668fe67f0e44a0ee20a5fec6c7de03c27d82 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -17,7 +17,7 @@ import * as resources from 'vs/base/common/resources'; import { defaultGenerator } from 'vs/base/common/idGenerator'; import types = require('vs/base/common/types'); import { Action, IAction } from 'vs/base/common/actions'; -import { IIconLabelOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Mode, IEntryRunContext, IAutoFocus, IQuickNavigateConfiguration, IModel } from 'vs/base/parts/quickopen/common/quickOpen'; import { QuickOpenEntry, QuickOpenModel, QuickOpenEntryGroup, compareEntries, QuickOpenItemAccessorClass } from 'vs/base/parts/quickopen/browser/quickOpenModel'; @@ -54,7 +54,7 @@ import { FileKind, IFileService } from 'vs/platform/files/common/files'; import { scoreItem, ScorerCache, compareItemsByScore, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; import { getBaseLabel } from 'vs/base/common/labels'; import { WorkbenchTree } from 'vs/platform/list/browser/listService'; -import { matchesFuzzyOcticonAware } from 'vs/base/common/filters'; +import { matchesFuzzyOcticonAware, removeOcticons } from 'vs/base/common/octicon'; const HELP_PREFIX = '?'; @@ -1025,6 +1025,7 @@ class PickOpenEntry extends PlaceholderQuickOpenEntry implements IPickOpenItem { private description: string; private detail: string; private tooltip: string; + private descriptionTooltip: string; private hasSeparator: boolean; private separatorLabel: string; private alwaysShow: boolean; @@ -1047,6 +1048,7 @@ class PickOpenEntry extends PlaceholderQuickOpenEntry implements IPickOpenItem { this.description = item.description; this.detail = item.detail; this.tooltip = item.tooltip; + this.descriptionTooltip = item.description ? removeOcticons(item.description) : void 0; this.hasSeparator = item.separator && item.separator.border; this.separatorLabel = item.separator && item.separator.label; this.alwaysShow = item.alwaysShow; @@ -1081,7 +1083,7 @@ class PickOpenEntry extends PlaceholderQuickOpenEntry implements IPickOpenItem { return this._index; } - public getLabelOptions(): IIconLabelOptions { + public getLabelOptions(): IIconLabelValueOptions { return { extraClasses: this.resource ? getIconClasses(this.modelService, this.modeService, this.resource, this.fileKind) : [] }; @@ -1103,6 +1105,10 @@ class PickOpenEntry extends PlaceholderQuickOpenEntry implements IPickOpenItem { return this.tooltip; } + public getDescriptionTooltip(): string { + return this.descriptionTooltip; + } + public showBorder(): boolean { return this.hasSeparator; } @@ -1282,7 +1288,7 @@ export class EditorHistoryEntry extends EditorQuickOpenEntry { return this.label; } - public getLabelOptions(): IIconLabelOptions { + public getLabelOptions(): IIconLabelValueOptions { return { extraClasses: getIconClasses(this.modelService, this.modeService, this.resource) }; diff --git a/src/vs/workbench/parts/search/browser/openFileHandler.ts b/src/vs/workbench/parts/search/browser/openFileHandler.ts index 04e8cad5150504f0df77546951bdfd8ed665930e..4fd687bbdbaad5bbbb80146f0fbb7b003acd4b23 100644 --- a/src/vs/workbench/parts/search/browser/openFileHandler.ts +++ b/src/vs/workbench/parts/search/browser/openFileHandler.ts @@ -13,7 +13,7 @@ import * as objects from 'vs/base/common/objects'; import { defaultGenerator } from 'vs/base/common/idGenerator'; import URI from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; -import { IIconLabelOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IModeService } from 'vs/editor/common/services/modeService'; import { getIconClasses } from 'vs/workbench/browser/labels'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -62,7 +62,7 @@ export class FileEntry extends EditorQuickOpenEntry { return this.name; } - public getLabelOptions(): IIconLabelOptions { + public getLabelOptions(): IIconLabelValueOptions { return { extraClasses: getIconClasses(this.modelService, this.modeService, this.resource) };