Archived
1
0

Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

57
lib/vscode/src/bootstrap-amd.js vendored Normal file
View File

@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const loader = require('./vs/loader');
const bootstrap = require('./bootstrap');
// Bootstrap: NLS
const nlsConfig = bootstrap.setupNLS();
// Bootstrap: Loader
loader.config({
baseUrl: bootstrap.fileUriFromPath(__dirname, { isWindows: process.platform === 'win32' }),
catchError: true,
nodeRequire: require,
nodeMain: __filename,
'vs/nls': nlsConfig
});
// Running in Electron
if (process.env['ELECTRON_RUN_AS_NODE'] || process.versions['electron']) {
loader.define('fs', ['original-fs'], function (originalFS) {
return originalFS; // replace the patched electron fs with the original node fs for all AMD code
});
}
// Pseudo NLS support
if (nlsConfig.pseudo) {
loader(['vs/nls'], function (nlsPlugin) {
nlsPlugin.setPseudoTranslation(nlsConfig.pseudo);
});
}
exports.load = function (entrypoint, onLoad, onError) {
if (!entrypoint) {
return;
}
// cached data config
if (process.env['VSCODE_NODE_CACHED_DATA_DIR']) {
loader.config({
nodeCachedData: {
path: process.env['VSCODE_NODE_CACHED_DATA_DIR'],
seed: entrypoint
}
});
}
onLoad = onLoad || function () { };
onError = onError || function (err) { console.error(err); };
loader([entrypoint], onLoad, onError);
};

189
lib/vscode/src/bootstrap-fork.js vendored Normal file
View File

@ -0,0 +1,189 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const bootstrap = require('./bootstrap');
const bootstrapNode = require('./bootstrap-node');
// Remove global paths from the node module lookup
bootstrapNode.removeGlobalNodeModuleLookupPaths();
// Enable ASAR in our forked processes
bootstrap.enableASARSupport();
if (process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']) {
bootstrapNode.injectNodeModuleLookupPath(process.env['VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH']);
}
// Configure: pipe logging to parent process
if (!!process.send && process.env.PIPE_LOGGING === 'true') {
pipeLoggingToParent();
}
// Handle Exceptions
if (!process.env['VSCODE_HANDLES_UNCAUGHT_ERRORS']) {
handleExceptions();
}
// Terminate when parent terminates
if (process.env['VSCODE_PARENT_PID']) {
terminateWhenParentTerminates();
}
// Configure Crash Reporter
configureCrashReporter();
// Load AMD entry point
require('./bootstrap-amd').load(process.env['AMD_ENTRYPOINT']);
//#region Helpers
function pipeLoggingToParent() {
const MAX_LENGTH = 100000;
// Prevent circular stringify and convert arguments to real array
function safeToArray(args) {
const seen = [];
const argsArray = [];
// Massage some arguments with special treatment
if (args.length) {
for (let i = 0; i < args.length; i++) {
// Any argument of type 'undefined' needs to be specially treated because
// JSON.stringify will simply ignore those. We replace them with the string
// 'undefined' which is not 100% right, but good enough to be logged to console
if (typeof args[i] === 'undefined') {
args[i] = 'undefined';
}
// Any argument that is an Error will be changed to be just the error stack/message
// itself because currently cannot serialize the error over entirely.
else if (args[i] instanceof Error) {
const errorObj = args[i];
if (errorObj.stack) {
args[i] = errorObj.stack;
} else {
args[i] = errorObj.toString();
}
}
argsArray.push(args[i]);
}
}
// Add the stack trace as payload if we are told so. We remove the message and the 2 top frames
// to start the stacktrace where the console message was being written
if (process.env.VSCODE_LOG_STACK === 'true') {
const stack = new Error().stack;
argsArray.push({ __$stack: stack.split('\n').slice(3).join('\n') });
}
try {
const res = JSON.stringify(argsArray, function (key, value) {
// Objects get special treatment to prevent circles
if (isObject(value) || Array.isArray(value)) {
if (seen.indexOf(value) !== -1) {
return '[Circular]';
}
seen.push(value);
}
return value;
});
if (res.length > MAX_LENGTH) {
return 'Output omitted for a large object that exceeds the limits';
}
return res;
} catch (error) {
return `Output omitted for an object that cannot be inspected ('${error.toString()}')`;
}
}
/**
* @param {{ type: string; severity: string; arguments: string; }} arg
*/
function safeSend(arg) {
try {
process.send(arg);
} catch (error) {
// Can happen if the parent channel is closed meanwhile
}
}
/**
* @param {unknown} obj
*/
function isObject(obj) {
return typeof obj === 'object'
&& obj !== null
&& !Array.isArray(obj)
&& !(obj instanceof RegExp)
&& !(obj instanceof Date);
}
// Pass console logging to the outside so that we have it in the main side if told so
if (process.env.VERBOSE_LOGGING === 'true') {
console.log = function () { safeSend({ type: '__$console', severity: 'log', arguments: safeToArray(arguments) }); };
console.info = function () { safeSend({ type: '__$console', severity: 'log', arguments: safeToArray(arguments) }); };
console.warn = function () { safeSend({ type: '__$console', severity: 'warn', arguments: safeToArray(arguments) }); };
} else {
console.log = function () { /* ignore */ };
console.warn = function () { /* ignore */ };
console.info = function () { /* ignore */ };
}
console.error = function () { safeSend({ type: '__$console', severity: 'error', arguments: safeToArray(arguments) }); };
}
function handleExceptions() {
// Handle uncaught exceptions
process.on('uncaughtException', function (err) {
console.error('Uncaught Exception: ', err);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', function (reason) {
console.error('Unhandled Promise Rejection: ', reason);
});
}
function terminateWhenParentTerminates() {
const parentPid = Number(process.env['VSCODE_PARENT_PID']);
if (typeof parentPid === 'number' && !isNaN(parentPid)) {
setInterval(function () {
try {
process.kill(parentPid, 0); // throws an exception if the main process doesn't exist anymore.
} catch (e) {
process.exit();
}
}, 5000);
}
}
function configureCrashReporter() {
const crashReporterOptionsRaw = process.env['CRASH_REPORTER_START_OPTIONS'];
if (typeof crashReporterOptionsRaw === 'string') {
try {
const crashReporterOptions = JSON.parse(crashReporterOptionsRaw);
if (crashReporterOptions) {
process['crashReporter'].start(crashReporterOptions);
}
} catch (error) {
console.error(error);
}
}
}
//#endregion

60
lib/vscode/src/bootstrap-node.js vendored Normal file
View File

@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
/**
* Add support for redirecting the loading of node modules
*
* @param {string} injectPath
*/
exports.injectNodeModuleLookupPath = function (injectPath) {
if (!injectPath) {
throw new Error('Missing injectPath');
}
const Module = require('module');
const path = require('path');
const nodeModulesPath = path.join(__dirname, '../node_modules');
// @ts-ignore
const originalResolveLookupPaths = Module._resolveLookupPaths;
// @ts-ignore
Module._resolveLookupPaths = function (moduleName, parent) {
const paths = originalResolveLookupPaths(moduleName, parent);
if (Array.isArray(paths)) {
for (let i = 0, len = paths.length; i < len; i++) {
if (paths[i] === nodeModulesPath) {
paths.splice(i, 0, injectPath);
break;
}
}
}
return paths;
};
};
exports.removeGlobalNodeModuleLookupPaths = function () {
const Module = require('module');
// @ts-ignore
const globalPaths = Module.globalPaths;
// @ts-ignore
const originalResolveLookupPaths = Module._resolveLookupPaths;
// @ts-ignore
Module._resolveLookupPaths = function (moduleName, parent) {
const paths = originalResolveLookupPaths(moduleName, parent);
let commonSuffixLength = 0;
while (commonSuffixLength < paths.length && paths[paths.length - 1 - commonSuffixLength] === globalPaths[globalPaths.length - 1 - commonSuffixLength]) {
commonSuffixLength++;
}
return paths.slice(0, paths.length - commonSuffixLength);
};
};

262
lib/vscode/src/bootstrap-window.js vendored Normal file
View File

@ -0,0 +1,262 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path="typings/require.d.ts" />
//@ts-check
'use strict';
// Simple module style to support node.js and browser environments
(function (globalThis, factory) {
// Node.js
if (typeof exports === 'object') {
module.exports = factory();
}
// Browser
else {
globalThis.MonacoBootstrapWindow = factory();
}
}(this, function () {
const bootstrapLib = bootstrap();
const preloadGlobals = globals();
const sandbox = preloadGlobals.context.sandbox;
const webFrame = preloadGlobals.webFrame;
const safeProcess = sandbox ? preloadGlobals.process : process;
/**
* @param {string[]} modulePaths
* @param {(result, configuration: object) => any} resultCallback
* @param {{ forceEnableDeveloperKeybindings?: boolean, disallowReloadKeybinding?: boolean, removeDeveloperKeybindingsAfterLoad?: boolean, canModifyDOM?: (config: object) => void, beforeLoaderConfig?: (config: object, loaderConfig: object) => void, beforeRequire?: () => void }=} options
*/
function load(modulePaths, resultCallback, options) {
const args = parseURLQueryArgs();
/**
* // configuration: INativeWindowConfiguration
* @type {{
* zoomLevel?: number,
* extensionDevelopmentPath?: string[],
* extensionTestsPath?: string,
* userEnv?: { [key: string]: string | undefined },
* appRoot?: string,
* nodeCachedDataDir?: string
* }} */
const configuration = JSON.parse(args['config'] || '{}') || {};
// Apply zoom level early to avoid glitches
const zoomLevel = configuration.zoomLevel;
if (typeof zoomLevel === 'number' && zoomLevel !== 0) {
webFrame.setZoomLevel(zoomLevel);
}
// Error handler
safeProcess.on('uncaughtException', function (error) {
onUnexpectedError(error, enableDeveloperTools);
});
// Developer tools
const enableDeveloperTools = (safeProcess.env['VSCODE_DEV'] || !!configuration.extensionDevelopmentPath) && !configuration.extensionTestsPath;
let developerToolsUnbind;
if (enableDeveloperTools || (options && options.forceEnableDeveloperKeybindings)) {
developerToolsUnbind = registerDeveloperKeybindings(options && options.disallowReloadKeybinding);
}
// Correctly inherit the parent's environment (TODO@sandbox non-sandboxed only)
if (!sandbox) {
Object.assign(safeProcess.env, configuration.userEnv);
}
// Enable ASAR support (TODO@sandbox non-sandboxed only)
if (!sandbox) {
globalThis.MonacoBootstrap.enableASARSupport(configuration.appRoot);
}
if (options && typeof options.canModifyDOM === 'function') {
options.canModifyDOM(configuration);
}
// Get the nls configuration into the process.env as early as possible (TODO@sandbox non-sandboxed only)
const nlsConfig = sandbox ? { availableLanguages: {} } : globalThis.MonacoBootstrap.setupNLS();
let locale = nlsConfig.availableLanguages['*'] || 'en';
if (locale === 'zh-tw') {
locale = 'zh-Hant';
} else if (locale === 'zh-cn') {
locale = 'zh-Hans';
}
window.document.documentElement.setAttribute('lang', locale);
// do not advertise AMD to avoid confusing UMD modules loaded with nodejs
if (!sandbox) {
window['define'] = undefined;
}
// replace the patched electron fs with the original node fs for all AMD code (TODO@sandbox non-sandboxed only)
if (!sandbox) {
require.define('fs', ['original-fs'], function (originalFS) { return originalFS; });
}
window['MonacoEnvironment'] = {};
const loaderConfig = {
baseUrl: `${bootstrapLib.fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === 'win32' })}/out`,
'vs/nls': nlsConfig
};
// Enable loading of node modules:
// - sandbox: we list paths of webpacked modules to help the loader
// - non-sandbox: we signal that any module that does not begin with
// `vs/` should be loaded using node.js require()
if (sandbox) {
loaderConfig.paths = {
'vscode-textmate': `../node_modules/vscode-textmate/release/main`,
'vscode-oniguruma': `../node_modules/vscode-oniguruma/release/main`,
'xterm': `../node_modules/xterm/lib/xterm.js`,
'xterm-addon-search': `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
'xterm-addon-unicode11': `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
'xterm-addon-webgl': `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
'iconv-lite-umd': `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
'jschardet': `../node_modules/jschardet/dist/jschardet.min.js`,
};
} else {
loaderConfig.amdModulesPattern = /^vs\//;
}
// cached data config
if (configuration.nodeCachedDataDir) {
loaderConfig.nodeCachedData = {
path: configuration.nodeCachedDataDir,
seed: modulePaths.join('')
};
}
if (options && typeof options.beforeLoaderConfig === 'function') {
options.beforeLoaderConfig(configuration, loaderConfig);
}
require.config(loaderConfig);
if (nlsConfig.pseudo) {
require(['vs/nls'], function (nlsPlugin) {
nlsPlugin.setPseudoTranslation(nlsConfig.pseudo);
});
}
if (options && typeof options.beforeRequire === 'function') {
options.beforeRequire();
}
require(modulePaths, result => {
try {
const callbackResult = resultCallback(result, configuration);
if (callbackResult && typeof callbackResult.then === 'function') {
callbackResult.then(() => {
if (developerToolsUnbind && options && options.removeDeveloperKeybindingsAfterLoad) {
developerToolsUnbind();
}
}, error => {
onUnexpectedError(error, enableDeveloperTools);
});
}
} catch (error) {
onUnexpectedError(error, enableDeveloperTools);
}
}, onUnexpectedError);
}
/**
* @returns {{[param: string]: string }}
*/
function parseURLQueryArgs() {
const search = window.location.search || '';
return search.split(/[?&]/)
.filter(function (param) { return !!param; })
.map(function (param) { return param.split('='); })
.filter(function (param) { return param.length === 2; })
.reduce(function (r, param) { r[param[0]] = decodeURIComponent(param[1]); return r; }, {});
}
/**
* @param {boolean} disallowReloadKeybinding
* @returns {() => void}
*/
function registerDeveloperKeybindings(disallowReloadKeybinding) {
const ipcRenderer = preloadGlobals.ipcRenderer;
const extractKey = function (e) {
return [
e.ctrlKey ? 'ctrl-' : '',
e.metaKey ? 'meta-' : '',
e.altKey ? 'alt-' : '',
e.shiftKey ? 'shift-' : '',
e.keyCode
].join('');
};
// Devtools & reload support
const TOGGLE_DEV_TOOLS_KB = (safeProcess.platform === 'darwin' ? 'meta-alt-73' : 'ctrl-shift-73'); // mac: Cmd-Alt-I, rest: Ctrl-Shift-I
const TOGGLE_DEV_TOOLS_KB_ALT = '123'; // F12
const RELOAD_KB = (safeProcess.platform === 'darwin' ? 'meta-82' : 'ctrl-82'); // mac: Cmd-R, rest: Ctrl-R
let listener = function (e) {
const key = extractKey(e);
if (key === TOGGLE_DEV_TOOLS_KB || key === TOGGLE_DEV_TOOLS_KB_ALT) {
ipcRenderer.send('vscode:toggleDevTools');
} else if (key === RELOAD_KB && !disallowReloadKeybinding) {
ipcRenderer.send('vscode:reloadWindow');
}
};
window.addEventListener('keydown', listener);
return function () {
if (listener) {
window.removeEventListener('keydown', listener);
listener = undefined;
}
};
}
/**
* @param {string | Error} error
* @param {boolean} [enableDeveloperTools]
*/
function onUnexpectedError(error, enableDeveloperTools) {
if (enableDeveloperTools) {
const ipcRenderer = preloadGlobals.ipcRenderer;
ipcRenderer.send('vscode:openDevTools');
}
console.error(`[uncaught exception]: ${error}`);
if (error && typeof error !== 'string' && error.stack) {
console.error(error.stack);
}
}
/**
* @return {{ fileUriFromPath: (path: string, config: { isWindows?: boolean, scheme?: string, fallbackAuthority?: string }) => string; }}
*/
function bootstrap() {
// @ts-ignore (defined in bootstrap.js)
return window.MonacoBootstrap;
}
/**
* @return {typeof import('./vs/base/parts/sandbox/electron-sandbox/globals')}
*/
function globals() {
// @ts-ignore (defined in globals.js)
return window.vscode;
}
return {
load,
globals
};
}));

274
lib/vscode/src/bootstrap.js vendored Normal file
View File

@ -0,0 +1,274 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
// Simple module style to support node.js and browser environments
(function (globalThis, factory) {
// Node.js
if (typeof exports === 'object') {
module.exports = factory();
}
// Browser
else {
globalThis.MonacoBootstrap = factory();
}
}(this, function () {
const Module = typeof require === 'function' ? require('module') : undefined;
const path = typeof require === 'function' ? require('path') : undefined;
const fs = typeof require === 'function' ? require('fs') : undefined;
//#region global bootstrapping
// increase number of stack frames(from 10, https://github.com/v8/v8/wiki/Stack-Trace-API)
Error.stackTraceLimit = 100;
// Workaround for Electron not installing a handler to ignore SIGPIPE
// (https://github.com/electron/electron/issues/13254)
if (typeof process !== 'undefined') {
process.on('SIGPIPE', () => {
console.error(new Error('Unexpected SIGPIPE'));
});
}
//#endregion
//#region Add support for using node_modules.asar
/**
* @param {string} appRoot
*/
function enableASARSupport(appRoot) {
if (!path || !Module) {
console.warn('enableASARSupport() is only available in node.js environments');
return;
}
let NODE_MODULES_PATH = appRoot ? path.join(appRoot, 'node_modules') : undefined;
if (!NODE_MODULES_PATH) {
NODE_MODULES_PATH = path.join(__dirname, '../node_modules');
} else {
// use the drive letter casing of __dirname
if (process.platform === 'win32') {
NODE_MODULES_PATH = __dirname.substr(0, 1) + NODE_MODULES_PATH.substr(1);
}
}
const NODE_MODULES_ASAR_PATH = `${NODE_MODULES_PATH}.asar`;
// @ts-ignore
const originalResolveLookupPaths = Module._resolveLookupPaths;
// @ts-ignore
Module._resolveLookupPaths = function (request, parent) {
const paths = originalResolveLookupPaths(request, parent);
if (Array.isArray(paths)) {
for (let i = 0, len = paths.length; i < len; i++) {
if (paths[i] === NODE_MODULES_PATH) {
paths.splice(i, 0, NODE_MODULES_ASAR_PATH);
break;
}
}
}
return paths;
};
}
//#endregion
//#region URI helpers
/**
* @param {string} path
* @param {{ isWindows?: boolean, scheme?: string, fallbackAuthority?: string }} config
* @returns {string}
*/
function fileUriFromPath(path, config) {
// Since we are building a URI, we normalize any backlsash
// to slashes and we ensure that the path begins with a '/'.
let pathName = path.replace(/\\/g, '/');
if (pathName.length > 0 && pathName.charAt(0) !== '/') {
pathName = `/${pathName}`;
}
/** @type {string} */
let uri;
// Windows: in order to support UNC paths (which start with '//')
// that have their own authority, we do not use the provided authority
// but rather preserve it.
if (config.isWindows && pathName.startsWith('//')) {
uri = encodeURI(`${config.scheme || 'file'}:${pathName}`);
}
// Otherwise we optionally add the provided authority if specified
else {
uri = encodeURI(`${config.scheme || 'file'}://${config.fallbackAuthority || ''}${pathName}`);
}
return uri.replace(/#/g, '%23');
}
//#endregion
//#region NLS helpers
/**
* @returns {{locale?: string, availableLanguages: {[lang: string]: string;}, pseudo?: boolean }}
*/
function setupNLS() {
if (!path || !fs) {
console.warn('setupNLS() is only available in node.js environments');
return;
}
// Get the nls configuration into the process.env as early as possible.
let nlsConfig = { availableLanguages: {} };
if (process.env['VSCODE_NLS_CONFIG']) {
try {
nlsConfig = JSON.parse(process.env['VSCODE_NLS_CONFIG']);
} catch (e) {
// Ignore
}
}
if (nlsConfig._resolvedLanguagePackCoreLocation) {
const bundles = Object.create(null);
nlsConfig.loadBundle = function (bundle, language, cb) {
const result = bundles[bundle];
if (result) {
cb(undefined, result);
return;
}
const bundleFile = path.join(nlsConfig._resolvedLanguagePackCoreLocation, `${bundle.replace(/\//g, '!')}.nls.json`);
fs.promises.readFile(bundleFile, 'utf8').then(function (content) {
const json = JSON.parse(content);
bundles[bundle] = json;
cb(undefined, json);
}).catch((error) => {
try {
if (nlsConfig._corruptedFile) {
fs.promises.writeFile(nlsConfig._corruptedFile, 'corrupted', 'utf8').catch(function (error) { console.error(error); });
}
} finally {
cb(error, undefined);
}
});
};
}
return nlsConfig;
}
//#endregion
//#region Portable helpers
/**
* @param {{ portable: string; applicationName: string; }} product
* @returns {{ portableDataPath: string; isPortable: boolean; }}
*/
function configurePortable(product) {
if (!path || !fs) {
console.warn('configurePortable() is only available in node.js environments');
return;
}
const appRoot = path.dirname(__dirname);
function getApplicationPath() {
if (process.env['VSCODE_DEV']) {
return appRoot;
}
if (process.platform === 'darwin') {
return path.dirname(path.dirname(path.dirname(appRoot)));
}
return path.dirname(path.dirname(appRoot));
}
function getPortableDataPath() {
if (process.env['VSCODE_PORTABLE']) {
return process.env['VSCODE_PORTABLE'];
}
if (process.platform === 'win32' || process.platform === 'linux') {
return path.join(getApplicationPath(), 'data');
}
// @ts-ignore
const portableDataName = product.portable || `${product.applicationName}-portable-data`;
return path.join(path.dirname(getApplicationPath()), portableDataName);
}
const portableDataPath = getPortableDataPath();
const isPortable = !('target' in product) && fs.existsSync(portableDataPath);
const portableTempPath = path.join(portableDataPath, 'tmp');
const isTempPortable = isPortable && fs.existsSync(portableTempPath);
if (isPortable) {
process.env['VSCODE_PORTABLE'] = portableDataPath;
} else {
delete process.env['VSCODE_PORTABLE'];
}
if (isTempPortable) {
if (process.platform === 'win32') {
process.env['TMP'] = portableTempPath;
process.env['TEMP'] = portableTempPath;
} else {
process.env['TMPDIR'] = portableTempPath;
}
}
return {
portableDataPath,
isPortable
};
}
//#endregion
//#region ApplicationInsights
// Prevents appinsights from monkey patching modules.
// This should be called before importing the applicationinsights module
function avoidMonkeyPatchFromAppInsights() {
if (typeof process === 'undefined') {
console.warn('avoidMonkeyPatchFromAppInsights() is only available in node.js environments');
return;
}
// @ts-ignore
process.env['APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL'] = true; // Skip monkey patching of 3rd party modules by appinsights
global['diagnosticsSource'] = {}; // Prevents diagnostic channel (which patches "require") from initializing entirely
}
//#endregion
return {
enableASARSupport,
avoidMonkeyPatchFromAppInsights,
configurePortable,
setupNLS,
fileUriFromPath
};
}));

View File

@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function entrypoint(name) {
return [{ name: name, include: [], exclude: ['vs/css', 'vs/nls'] }];
}
exports.base = [{
name: 'vs/base/common/worker/simpleWorker',
include: ['vs/editor/common/services/editorSimpleWorker'],
prepend: ['vs/loader.js'],
append: ['vs/base/worker/workerMain'],
dest: 'vs/base/worker/workerMain.js'
}];
exports.workerExtensionHost = [entrypoint('vs/workbench/services/extensions/worker/extensionHostWorker')];
exports.workerNotebook = [entrypoint('vs/workbench/contrib/notebook/common/services/notebookSimpleWorker')];
exports.workbenchDesktop = require('./vs/workbench/buildfile.desktop').collectModules();
exports.workbenchWeb = require('./vs/workbench/buildfile.web').collectModules();
exports.keyboardMaps = [
entrypoint('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux'),
entrypoint('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.darwin'),
entrypoint('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win')
];
exports.code = require('./vs/code/buildfile').collectModules();
exports.entrypoint = entrypoint;

22
lib/vscode/src/cli.js Normal file
View File

@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const bootstrap = require('./bootstrap');
const product = require('../product.json');
// Avoid Monkey Patches from Application Insights
bootstrap.avoidMonkeyPatchFromAppInsights();
// Enable portable support
bootstrap.configurePortable(product);
// Enable ASAR support
bootstrap.enableASARSupport();
// Load CLI through AMD loader
require('./bootstrap-amd').load('vs/code/node/cli');

596
lib/vscode/src/main.js Normal file
View File

@ -0,0 +1,596 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const perf = require('./vs/base/common/performance');
const lp = require('./vs/base/node/languagePacks');
perf.mark('main:started');
const path = require('path');
const fs = require('fs');
const os = require('os');
const bootstrap = require('./bootstrap');
const paths = require('./paths');
/** @type {any} */
const product = require('../product.json');
const { app, protocol, crashReporter } = require('electron');
// Disable render process reuse, we still have
// non-context aware native modules in the renderer.
app.allowRendererProcessReuse = false;
// Enable portable support
const portable = bootstrap.configurePortable(product);
// Enable ASAR support
bootstrap.enableASARSupport();
// Set userData path before app 'ready' event
const args = parseCLIArgs();
const userDataPath = getUserDataPath(args);
app.setPath('userData', userDataPath);
// Configure static command line arguments
const argvConfig = configureCommandlineSwitchesSync(args);
// If a crash-reporter-directory is specified we store the crash reports
// in the specified directory and don't upload them to the crash server.
let crashReporterDirectory = args['crash-reporter-directory'];
let submitURL = '';
if (crashReporterDirectory) {
crashReporterDirectory = path.normalize(crashReporterDirectory);
if (!path.isAbsolute(crashReporterDirectory)) {
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory must be absolute.`);
app.exit(1);
}
if (!fs.existsSync(crashReporterDirectory)) {
try {
fs.mkdirSync(crashReporterDirectory);
} catch (error) {
console.error(`The path '${crashReporterDirectory}' specified for --crash-reporter-directory does not seem to exist or cannot be created.`);
app.exit(1);
}
}
// Crashes are stored in the crashDumps directory by default, so we
// need to change that directory to the provided one
console.log(`Found --crash-reporter-directory argument. Setting crashDumps directory to be '${crashReporterDirectory}'`);
app.setPath('crashDumps', crashReporterDirectory);
} else {
const appCenter = product.appCenter;
// Disable Appcenter crash reporting if
// * --crash-reporter-directory is specified
// * enable-crash-reporter runtime argument is set to 'false'
// * --disable-crash-reporter command line parameter is set
if (appCenter && argvConfig['enable-crash-reporter'] && !args['disable-crash-reporter']) {
const isWindows = (process.platform === 'win32');
const isLinux = (process.platform === 'linux');
const crashReporterId = argvConfig['crash-reporter-id'];
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (uuidPattern.test(crashReporterId)) {
submitURL = isWindows ? appCenter[process.arch === 'ia32' ? 'win32-ia32' : 'win32-x64'] : isLinux ? appCenter[`linux-x64`] : appCenter.darwin;
submitURL = submitURL.concat('&uid=', crashReporterId, '&iid=', crashReporterId, '&sid=', crashReporterId);
// Send the id for child node process that are explicitly starting crash reporter.
// For vscode this is ExtensionHost process currently.
const argv = process.argv;
const endOfArgsMarkerIndex = argv.indexOf('--');
if (endOfArgsMarkerIndex === -1) {
argv.push('--crash-reporter-id', crashReporterId);
} else {
// if the we have an argument "--" (end of argument marker)
// we cannot add arguments at the end. rather, we add
// arguments before the "--" marker.
argv.splice(endOfArgsMarkerIndex, 0, '--crash-reporter-id', crashReporterId);
}
}
}
}
// Start crash reporter for all processes
const productName = (product.crashReporter ? product.crashReporter.productName : undefined) || product.nameShort;
const companyName = (product.crashReporter ? product.crashReporter.companyName : undefined) || 'Microsoft';
crashReporter.start({
companyName: companyName,
productName: process.env['VSCODE_DEV'] ? `${productName} Dev` : productName,
submitURL,
uploadToServer: !crashReporterDirectory
});
// Set logs path before app 'ready' event if running portable
// to ensure that no 'logs' folder is created on disk at a
// location outside of the portable directory
// (https://github.com/microsoft/vscode/issues/56651)
if (portable.isPortable) {
app.setAppLogsPath(path.join(userDataPath, 'logs'));
}
// Update cwd based on environment and platform
setCurrentWorkingDirectory();
// Register custom schemes with privileges
protocol.registerSchemesAsPrivileged([
{
scheme: 'vscode-webview',
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
corsEnabled: true,
}
}, {
scheme: 'vscode-webview-resource',
privileges: {
secure: true,
standard: true,
supportFetchAPI: true,
corsEnabled: true,
}
},
]);
// Global app listeners
registerListeners();
// Cached data
const nodeCachedDataDir = getNodeCachedDir();
/**
* Support user defined locale: load it early before app('ready')
* to have more things running in parallel.
*
* @type {Promise<import('./vs/base/node/languagePacks').NLSConfiguration>} nlsConfig | undefined
*/
let nlsConfigurationPromise = undefined;
const metaDataFile = path.join(__dirname, 'nls.metadata.json');
const locale = getUserDefinedLocale(argvConfig);
if (locale) {
nlsConfigurationPromise = lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale);
}
// Load our code once ready
app.once('ready', function () {
if (args['trace']) {
const contentTracing = require('electron').contentTracing;
const traceOptions = {
categoryFilter: args['trace-category-filter'] || '*',
traceOptions: args['trace-options'] || 'record-until-full,enable-sampling'
};
contentTracing.startRecording(traceOptions).finally(() => onReady());
} else {
onReady();
}
});
/**
* Main startup routine
*
* @param {string | undefined} cachedDataDir
* @param {import('./vs/base/node/languagePacks').NLSConfiguration} nlsConfig
*/
function startup(cachedDataDir, nlsConfig) {
nlsConfig._languagePackSupport = true;
process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || '';
// Load main in AMD
perf.mark('willLoadMainBundle');
require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
perf.mark('didLoadMainBundle');
});
}
async function onReady() {
perf.mark('main:appReady');
try {
const [cachedDataDir, nlsConfig] = await Promise.all([nodeCachedDataDir.ensureExists(), resolveNlsConfiguration()]);
startup(cachedDataDir, nlsConfig);
} catch (error) {
console.error(error);
}
}
/**
* @typedef {{ [arg: string]: any; '--'?: string[]; _: string[]; }} NativeParsedArgs
*
* @param {NativeParsedArgs} cliArgs
*/
function configureCommandlineSwitchesSync(cliArgs) {
const SUPPORTED_ELECTRON_SWITCHES = [
// alias from us for --disable-gpu
'disable-hardware-acceleration',
// provided by Electron
'disable-color-correct-rendering',
// override for the color profile to use
'force-color-profile'
];
if (process.platform === 'linux') {
// Force enable screen readers on Linux via this flag
SUPPORTED_ELECTRON_SWITCHES.push('force-renderer-accessibility');
}
const SUPPORTED_MAIN_PROCESS_SWITCHES = [
// Persistently enable proposed api via argv.json: https://github.com/microsoft/vscode/issues/99775
'enable-proposed-api'
];
// Read argv config
const argvConfig = readArgvConfigSync();
Object.keys(argvConfig).forEach(argvKey => {
const argvValue = argvConfig[argvKey];
// Append Electron flags to Electron
if (SUPPORTED_ELECTRON_SWITCHES.indexOf(argvKey) !== -1) {
// Color profile
if (argvKey === 'force-color-profile') {
if (argvValue) {
app.commandLine.appendSwitch(argvKey, argvValue);
}
}
// Others
else if (argvValue === true || argvValue === 'true') {
if (argvKey === 'disable-hardware-acceleration') {
app.disableHardwareAcceleration(); // needs to be called explicitly
} else {
app.commandLine.appendSwitch(argvKey);
}
}
}
// Append main process flags to process.argv
else if (SUPPORTED_MAIN_PROCESS_SWITCHES.indexOf(argvKey) !== -1) {
if (argvKey === 'enable-proposed-api') {
if (Array.isArray(argvValue)) {
argvValue.forEach(id => id && typeof id === 'string' && process.argv.push('--enable-proposed-api', id));
} else {
console.error(`Unexpected value for \`enable-proposed-api\` in argv.json. Expected array of extension ids.`);
}
}
}
});
// Support JS Flags
const jsFlags = getJSFlags(cliArgs);
if (jsFlags) {
app.commandLine.appendSwitch('js-flags', jsFlags);
}
return argvConfig;
}
function readArgvConfigSync() {
// Read or create the argv.json config file sync before app('ready')
const argvConfigPath = getArgvConfigPath();
let argvConfig;
try {
argvConfig = JSON.parse(stripComments(fs.readFileSync(argvConfigPath).toString()));
} catch (error) {
if (error && error.code === 'ENOENT') {
createDefaultArgvConfigSync(argvConfigPath);
} else {
console.warn(`Unable to read argv.json configuration file in ${argvConfigPath}, falling back to defaults (${error})`);
}
}
// Fallback to default
if (!argvConfig) {
argvConfig = {
'disable-color-correct-rendering': true // Force pre-Chrome-60 color profile handling (for https://github.com/microsoft/vscode/issues/51791)
};
}
return argvConfig;
}
/**
* @param {string} argvConfigPath
*/
function createDefaultArgvConfigSync(argvConfigPath) {
try {
// Ensure argv config parent exists
const argvConfigPathDirname = path.dirname(argvConfigPath);
if (!fs.existsSync(argvConfigPathDirname)) {
fs.mkdirSync(argvConfigPathDirname);
}
// Default argv content
const defaultArgvConfigContent = [
'// This configuration file allows you to pass permanent command line arguments to VS Code.',
'// Only a subset of arguments is currently supported to reduce the likelihood of breaking',
'// the installation.',
'//',
'// PLEASE DO NOT CHANGE WITHOUT UNDERSTANDING THE IMPACT',
'//',
'// NOTE: Changing this file requires a restart of VS Code.',
'{',
' // Use software rendering instead of hardware accelerated rendering.',
' // This can help in cases where you see rendering issues in VS Code.',
' // "disable-hardware-acceleration": true,',
'',
' // Enabled by default by VS Code to resolve color issues in the renderer',
' // See https://github.com/microsoft/vscode/issues/51791 for details',
' "disable-color-correct-rendering": true',
'}'
];
// Create initial argv.json with default content
fs.writeFileSync(argvConfigPath, defaultArgvConfigContent.join('\n'));
} catch (error) {
console.error(`Unable to create argv.json configuration file in ${argvConfigPath}, falling back to defaults (${error})`);
}
}
function getArgvConfigPath() {
const vscodePortable = process.env['VSCODE_PORTABLE'];
if (vscodePortable) {
return path.join(vscodePortable, 'argv.json');
}
let dataFolderName = product.dataFolderName;
if (process.env['VSCODE_DEV']) {
dataFolderName = `${dataFolderName}-dev`;
}
return path.join(os.homedir(), dataFolderName, 'argv.json');
}
/**
* @param {NativeParsedArgs} cliArgs
* @returns {string}
*/
function getJSFlags(cliArgs) {
const jsFlags = [];
// Add any existing JS flags we already got from the command line
if (cliArgs['js-flags']) {
jsFlags.push(cliArgs['js-flags']);
}
// Support max-memory flag
if (cliArgs['max-memory'] && !/max_old_space_size=(\d+)/g.exec(cliArgs['js-flags'])) {
jsFlags.push(`--max_old_space_size=${cliArgs['max-memory']}`);
}
return jsFlags.length > 0 ? jsFlags.join(' ') : null;
}
/**
* @param {NativeParsedArgs} cliArgs
*
* @returns {string}
*/
function getUserDataPath(cliArgs) {
if (portable.isPortable) {
return path.join(portable.portableDataPath, 'user-data');
}
return path.resolve(cliArgs['user-data-dir'] || paths.getDefaultUserDataPath(process.platform));
}
/**
* @returns {NativeParsedArgs}
*/
function parseCLIArgs() {
const minimist = require('minimist');
return minimist(process.argv, {
string: [
'user-data-dir',
'locale',
'js-flags',
'max-memory',
'crash-reporter-directory'
]
});
}
function setCurrentWorkingDirectory() {
try {
if (process.platform === 'win32') {
process.env['VSCODE_CWD'] = process.cwd(); // remember as environment variable
process.chdir(path.dirname(app.getPath('exe'))); // always set application folder as cwd
} else if (process.env['VSCODE_CWD']) {
process.chdir(process.env['VSCODE_CWD']);
}
} catch (err) {
console.error(err);
}
}
function registerListeners() {
/**
* macOS: when someone drops a file to the not-yet running VSCode, the open-file event fires even before
* the app-ready event. We listen very early for open-file and remember this upon startup as path to open.
*
* @type {string[]}
*/
const macOpenFiles = [];
global['macOpenFiles'] = macOpenFiles;
app.on('open-file', function (event, path) {
macOpenFiles.push(path);
});
/**
* macOS: react to open-url requests.
*
* @type {string[]}
*/
const openUrls = [];
const onOpenUrl = function (event, url) {
event.preventDefault();
openUrls.push(url);
};
app.on('will-finish-launching', function () {
app.on('open-url', onOpenUrl);
});
global['getOpenUrls'] = function () {
app.removeListener('open-url', onOpenUrl);
return openUrls;
};
}
/**
* @returns {{ ensureExists: () => Promise<string | undefined> }}
*/
function getNodeCachedDir() {
return new class {
constructor() {
this.value = this._compute();
}
async ensureExists() {
try {
await mkdirp(this.value);
return this.value;
} catch (error) {
// ignore
}
}
_compute() {
if (process.argv.indexOf('--no-cached-data') > 0) {
return undefined;
}
// IEnvironmentService.isBuilt
if (process.env['VSCODE_DEV']) {
return undefined;
}
// find commit id
const commit = product.commit;
if (!commit) {
return undefined;
}
return path.join(userDataPath, 'CachedData', commit);
}
};
}
/**
* @param {string} dir
* @returns {Promise<string>}
*/
function mkdirp(dir) {
const fs = require('fs');
return new Promise((resolve, reject) => {
fs.mkdir(dir, { recursive: true }, err => (err && err.code !== 'EEXIST') ? reject(err) : resolve(dir));
});
}
//#region NLS Support
/**
* Resolve the NLS configuration
*
* @return {Promise<import('./vs/base/node/languagePacks').NLSConfiguration>}
*/
async function resolveNlsConfiguration() {
// First, we need to test a user defined locale. If it fails we try the app locale.
// If that fails we fall back to English.
let nlsConfiguration = nlsConfigurationPromise ? await nlsConfigurationPromise : undefined;
if (!nlsConfiguration) {
// Try to use the app locale. Please note that the app locale is only
// valid after we have received the app ready event. This is why the
// code is here.
let appLocale = app.getLocale();
if (!appLocale) {
nlsConfiguration = { locale: 'en', availableLanguages: {} };
} else {
// See above the comment about the loader and case sensitiviness
appLocale = appLocale.toLowerCase();
nlsConfiguration = await lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale);
if (!nlsConfiguration) {
nlsConfiguration = { locale: appLocale, availableLanguages: {} };
}
}
} else {
// We received a valid nlsConfig from a user defined locale
}
return nlsConfiguration;
}
/**
* @param {string} content
* @returns {string}
*/
function stripComments(content) {
const regexp = /("(?:[^\\"]*(?:\\.)?)*")|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g;
return content.replace(regexp, function (match, m1, m2, m3, m4) {
// Only one of m1, m2, m3, m4 matches
if (m3) {
// A block comment. Replace with nothing
return '';
} else if (m4) {
// A line comment. If it ends in \r?\n then keep it.
const length_1 = m4.length;
if (length_1 > 2 && m4[length_1 - 1] === '\n') {
return m4[length_1 - 2] === '\r' ? '\r\n' : '\n';
}
else {
return '';
}
} else {
// We match a string
return match;
}
});
}
/**
* Language tags are case insensitive however an amd loader is case sensitive
* To make this work on case preserving & insensitive FS we do the following:
* the language bundles have lower case language tags and we always lower case
* the locale we receive from the user or OS.
*
* @param {{ locale: string | undefined; }} argvConfig
* @returns {string | undefined}
*/
function getUserDefinedLocale(argvConfig) {
const locale = args['locale'];
if (locale) {
return locale.toLowerCase(); // a directly provided --locale always wins
}
return argvConfig.locale && typeof argvConfig.locale === 'string' ? argvConfig.locale.toLowerCase() : undefined;
}
//#endregion

35
lib/vscode/src/paths.js Normal file
View File

@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
const pkg = require('../package.json');
const path = require('path');
const os = require('os');
/**
* @param {string} platform
* @returns {string}
*/
function getAppDataPath(platform) {
switch (platform) {
case 'win32': return process.env['VSCODE_APPDATA'] || process.env['APPDATA'] || path.join(process.env['USERPROFILE'], 'AppData', 'Roaming');
case 'darwin': return process.env['VSCODE_APPDATA'] || path.join(os.homedir(), 'Library', 'Application Support');
case 'linux': return process.env['VSCODE_APPDATA'] || process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
default: throw new Error('Platform not supported');
}
}
/**
* @param {string} platform
* @returns {string}
*/
function getDefaultUserDataPath(platform) {
return path.join(getAppDataPath(platform), pkg.name);
}
exports.getAppDataPath = getAppDataPath;
exports.getDefaultUserDataPath = getDefaultUserDataPath;

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"module": "amd",
"moduleResolution": "node",
"experimentalDecorators": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"allowUnreachableCode": false,
"strict": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"vs/*": [
"./vs/*"
]
},
"lib": [
"ES2015",
"ES2016.Array.Include",
"ES2017.String",
"ES2018.Promise",
"DOM",
"DOM.Iterable",
"WebWorker.ImportScripts"
]
}
}

View File

@ -0,0 +1,22 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"removeComments": false,
"preserveConstEnums": true,
"sourceMap": false,
"outDir": "../out/vs",
"target": "es2017",
"types": [
"keytar",
"mocha",
"semver",
"sinon",
"winreg",
"trusted-types"
]
},
"include": [
"./typings",
"./vs"
]
}

View File

@ -0,0 +1,32 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"noEmit": true,
"types": [
"trusted-types"
],
"paths": {},
"module": "amd",
"moduleResolution": "classic",
"removeComments": false,
"preserveConstEnums": true,
"target": "es6",
"sourceMap": false,
"declaration": true
},
"include": [
"typings/require.d.ts",
"typings/thenable.d.ts",
"vs/css.d.ts",
"vs/monaco.d.ts",
"vs/nls.d.ts",
"vs/editor/*",
"vs/base/common/*",
"vs/base/browser/*",
"vs/platform/*/common/*",
"vs/platform/*/browser/*"
],
"exclude": [
"node_modules/*"
]
}

56
lib/vscode/src/typings/require.d.ts vendored Normal file
View File

@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare const enum LoaderEventType {
LoaderAvailable = 1,
BeginLoadingScript = 10,
EndLoadingScriptOK = 11,
EndLoadingScriptError = 12,
BeginInvokeFactory = 21,
EndInvokeFactory = 22,
NodeBeginEvaluatingScript = 31,
NodeEndEvaluatingScript = 32,
NodeBeginNativeRequire = 33,
NodeEndNativeRequire = 34,
CachedDataFound = 60,
CachedDataMissed = 61,
CachedDataRejected = 62,
CachedDataCreated = 63,
}
declare class LoaderEvent {
readonly type: LoaderEventType;
readonly timestamp: number;
readonly detail: string;
}
declare const define: {
(moduleName: string, dependencies: string[], callback: (...args: any[]) => any): any;
(moduleName: string, dependencies: string[], definition: any): any;
(moduleName: string, callback: (...args: any[]) => any): any;
(moduleName: string, definition: any): any;
(dependencies: string[], callback: (...args: any[]) => any): any;
(dependencies: string[], definition: any): any;
};
interface NodeRequire {
/**
* @deprecated use `FileAccess.asFileUri()` for node.js contexts or `FileAccess.asBrowserUri` for browser contexts.
*/
toUrl(path: string): string;
(dependencies: string[], callback: (...args: any[]) => any, errorback?: (err: any) => void): any;
config(data: any): any;
onError: Function;
__$__nodeRequire<T>(moduleName: string): T;
getStats(): ReadonlyArray<LoaderEvent>;
define(amdModuleId: string, dependencies: string[], callback: (...args: any[]) => any): any;
}
declare var require: NodeRequire;

21
lib/vscode/src/typings/thenable.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise,
* and others. This API makes no assumption about what promise libary is being used which
* enables reusing existing code without migrating to a specific promise implementation. Still,
* we recommend the use of native promises which are available in VS Code.
*/
interface Thenable<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
}

View File

@ -0,0 +1,122 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
class WindowManager {
public static readonly INSTANCE = new WindowManager();
// --- Zoom Level
private _zoomLevel: number = 0;
private _lastZoomLevelChangeTime: number = 0;
private readonly _onDidChangeZoomLevel = new Emitter<number>();
public readonly onDidChangeZoomLevel: Event<number> = this._onDidChangeZoomLevel.event;
public getZoomLevel(): number {
return this._zoomLevel;
}
public getTimeSinceLastZoomLevelChanged(): number {
return Date.now() - this._lastZoomLevelChangeTime;
}
public setZoomLevel(zoomLevel: number, isTrusted: boolean): void {
if (this._zoomLevel === zoomLevel) {
return;
}
this._zoomLevel = zoomLevel;
// See https://github.com/microsoft/vscode/issues/26151
this._lastZoomLevelChangeTime = isTrusted ? 0 : Date.now();
this._onDidChangeZoomLevel.fire(this._zoomLevel);
}
// --- Zoom Factor
private _zoomFactor: number = 1;
public getZoomFactor(): number {
return this._zoomFactor;
}
public setZoomFactor(zoomFactor: number): void {
this._zoomFactor = zoomFactor;
}
// --- Pixel Ratio
public getPixelRatio(): number {
let ctx: any = document.createElement('canvas').getContext('2d');
let dpr = window.devicePixelRatio || 1;
let bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
}
// --- Fullscreen
private _fullscreen: boolean = false;
private readonly _onDidChangeFullscreen = new Emitter<void>();
public readonly onDidChangeFullscreen: Event<void> = this._onDidChangeFullscreen.event;
public setFullscreen(fullscreen: boolean): void {
if (this._fullscreen === fullscreen) {
return;
}
this._fullscreen = fullscreen;
this._onDidChangeFullscreen.fire();
}
public isFullscreen(): boolean {
return this._fullscreen;
}
}
/** A zoom index, e.g. 1, 2, 3 */
export function setZoomLevel(zoomLevel: number, isTrusted: boolean): void {
WindowManager.INSTANCE.setZoomLevel(zoomLevel, isTrusted);
}
export function getZoomLevel(): number {
return WindowManager.INSTANCE.getZoomLevel();
}
/** Returns the time (in ms) since the zoom level was changed */
export function getTimeSinceLastZoomLevelChanged(): number {
return WindowManager.INSTANCE.getTimeSinceLastZoomLevelChanged();
}
export function onDidChangeZoomLevel(callback: (zoomLevel: number) => void): IDisposable {
return WindowManager.INSTANCE.onDidChangeZoomLevel(callback);
}
/** The zoom scale for an index, e.g. 1, 1.2, 1.4 */
export function getZoomFactor(): number {
return WindowManager.INSTANCE.getZoomFactor();
}
export function setZoomFactor(zoomFactor: number): void {
WindowManager.INSTANCE.setZoomFactor(zoomFactor);
}
export function getPixelRatio(): number {
return WindowManager.INSTANCE.getPixelRatio();
}
export function setFullscreen(fullscreen: boolean): void {
WindowManager.INSTANCE.setFullscreen(fullscreen);
}
export function isFullscreen(): boolean {
return WindowManager.INSTANCE.isFullscreen();
}
export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscreen;
const userAgent = navigator.userAgent;
export const isEdge = (userAgent.indexOf('Edge/') >= 0);
export const isOpera = (userAgent.indexOf('Opera') >= 0);
export const isFirefox = (userAgent.indexOf('Firefox') >= 0);
export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
export const isChrome = (userAgent.indexOf('Chrome') >= 0);
export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
export const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
export const isIPad = (userAgent.indexOf('iPad') >= 0 || (isSafari && navigator.maxTouchPoints > 0));
export const isEdgeWebView = isEdge && (userAgent.indexOf('WebView/') >= 0);
export const isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches);

View File

@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as browser from 'vs/base/browser/browser';
import * as platform from 'vs/base/common/platform';
export const enum KeyboardSupport {
Always,
FullScreen,
None
}
/**
* Browser feature we can support in current platform, browser and environment.
*/
export const BrowserFeatures = {
clipboard: {
writeText: (
platform.isNative
|| (document.queryCommandSupported && document.queryCommandSupported('copy'))
|| !!(navigator && navigator.clipboard && navigator.clipboard.writeText)
),
readText: (
platform.isNative
|| !!(navigator && navigator.clipboard && navigator.clipboard.readText)
),
richText: (() => {
if (browser.isEdge) {
let index = navigator.userAgent.indexOf('Edge/');
let version = parseInt(navigator.userAgent.substring(index + 5, navigator.userAgent.indexOf('.', index)), 10);
if (!version || (version >= 12 && version <= 16)) {
return false;
}
}
return true;
})()
},
keyboard: (() => {
if (platform.isNative || browser.isStandalone) {
return KeyboardSupport.Always;
}
if ((<any>navigator).keyboard || browser.isSafari) {
return KeyboardSupport.FullScreen;
}
return KeyboardSupport.None;
})(),
// 'ontouchstart' in window always evaluates to true with typescript's modern typings. This causes `window` to be
// `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast
touch: 'ontouchstart' in window || navigator.maxTouchPoints > 0 || (window as Window).navigator.msMaxTouchPoints > 0,
pointerEvents: window.PointerEvent && ('ontouchstart' in window || (window as Window).navigator.maxTouchPoints > 0 || navigator.maxTouchPoints > 0 || (window as Window).navigator.msMaxTouchPoints > 0)
};

View File

@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
const renderCodiconsRegex = /(\\)?\$\((([a-z0-9\-]+?)(?:~([a-z0-9\-]*?))?)\)/gi;
export function renderCodicons(text: string): Array<HTMLSpanElement | string> {
const elements = new Array<HTMLSpanElement | string>();
let match: RegExpMatchArray | null;
let textStart = 0, textStop = 0;
while ((match = renderCodiconsRegex.exec(text)) !== null) {
textStop = match.index || 0;
elements.push(text.substring(textStart, textStop));
textStart = (match.index || 0) + match[0].length;
const [, escaped, codicon, name, animation] = match;
elements.push(escaped ? `$(${codicon})` : renderCodicon(name, animation));
}
if (textStart < text.length) {
elements.push(text.substring(textStart));
}
return elements;
}
export function renderCodicon(name: string, animation: string): HTMLSpanElement {
return dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`);
}

View File

@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IAction, IActionRunner, IActionViewItem } from 'vs/base/common/actions';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
export interface IContextMenuEvent {
readonly shiftKey?: boolean;
readonly ctrlKey?: boolean;
readonly altKey?: boolean;
readonly metaKey?: boolean;
}
export interface IContextMenuDelegate {
getAnchor(): HTMLElement | { x: number; y: number; width?: number; height?: number; };
getActions(): IAction[];
getCheckedActionsRepresentation?(action: IAction): 'radio' | 'checkbox';
getActionViewItem?(action: IAction): IActionViewItem | undefined;
getActionsContext?(event?: IContextMenuEvent): any;
getKeyBinding?(action: IAction): ResolvedKeybinding | undefined;
getMenuClassName?(): string;
onHide?(didCancel: boolean): void;
actionRunner?: IActionRunner;
autoSelectFirstItem?: boolean;
anchorAlignment?: AnchorAlignment;
domForShadowRoot?: HTMLElement;
}
export interface IContextMenuProvider {
showContextMenu(delegate: IContextMenuDelegate): void;
}

View File

@ -0,0 +1,114 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { addDisposableListener } from 'vs/base/browser/dom';
/**
* A helper that will execute a provided function when the provided HTMLElement receives
* dragover event for 800ms. If the drag is aborted before, the callback will not be triggered.
*/
export class DelayedDragHandler extends Disposable {
private timeout: any;
constructor(container: HTMLElement, callback: () => void) {
super();
this._register(addDisposableListener(container, 'dragover', e => {
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
if (!this.timeout) {
this.timeout = setTimeout(() => {
callback();
this.timeout = null;
}, 800);
}
}));
['dragleave', 'drop', 'dragend'].forEach(type => {
this._register(addDisposableListener(container, type, () => {
this.clearDragTimeout();
}));
});
}
private clearDragTimeout(): void {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
}
dispose(): void {
super.dispose();
this.clearDragTimeout();
}
}
// Common data transfers
export const DataTransfers = {
/**
* Application specific resource transfer type
*/
RESOURCES: 'ResourceURLs',
/**
* Browser specific transfer type to download
*/
DOWNLOAD_URL: 'DownloadURL',
/**
* Browser specific transfer type for files
*/
FILES: 'Files',
/**
* Typically transfer type for copy/paste transfers.
*/
TEXT: 'text/plain'
};
export function applyDragImage(event: DragEvent, label: string | null, clazz: string): void {
const dragImage = document.createElement('div');
dragImage.className = clazz;
dragImage.textContent = label;
if (event.dataTransfer) {
document.body.appendChild(dragImage);
event.dataTransfer.setDragImage(dragImage, -10, -10);
// Removes the element when the DND operation is done
setTimeout(() => document.body.removeChild(dragImage), 0);
}
}
export interface IDragAndDropData {
update(dataTransfer: DataTransfer): void;
getData(): any;
}
export class DragAndDropData<T> implements IDragAndDropData {
constructor(private data: T) { }
update(): void {
// noop
}
getData(): T {
return this.data;
}
}
export interface IStaticDND {
CurrentDragAndDropData: IDragAndDropData | undefined;
}
export const StaticDND: IStaticDND = {
CurrentDragAndDropData: undefined
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event as BaseEvent, Emitter } from 'vs/base/common/event';
export type EventHandler = HTMLElement | HTMLDocument | Window;
export interface IDomEvent {
<K extends keyof HTMLElementEventMap>(element: EventHandler, type: K, useCapture?: boolean): BaseEvent<HTMLElementEventMap[K]>;
(element: EventHandler, type: string, useCapture?: boolean): BaseEvent<any>;
}
export const domEvent: IDomEvent = (element: EventHandler, type: string, useCapture?: boolean) => {
const fn = (e: Event) => emitter.fire(e);
const emitter = new Emitter<Event>({
onFirstListenerAdd: () => {
element.addEventListener(type, fn, useCapture);
},
onLastListenerRemove: () => {
element.removeEventListener(type, fn, useCapture);
}
});
return emitter.event;
};
export interface CancellableEvent {
preventDefault(): void;
stopPropagation(): void;
}
export function stop<T extends CancellableEvent>(event: BaseEvent<T>): BaseEvent<T> {
return BaseEvent.map(event, e => {
e.preventDefault();
e.stopPropagation();
return e;
});
}

View File

@ -0,0 +1,256 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class FastDomNode<T extends HTMLElement> {
public readonly domNode: T;
private _maxWidth: number;
private _width: number;
private _height: number;
private _top: number;
private _left: number;
private _bottom: number;
private _right: number;
private _fontFamily: string;
private _fontWeight: string;
private _fontSize: number;
private _fontFeatureSettings: string;
private _lineHeight: number;
private _letterSpacing: number;
private _className: string;
private _display: string;
private _position: string;
private _visibility: string;
private _backgroundColor: string;
private _layerHint: boolean;
private _contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint';
private _boxShadow: string;
constructor(domNode: T) {
this.domNode = domNode;
this._maxWidth = -1;
this._width = -1;
this._height = -1;
this._top = -1;
this._left = -1;
this._bottom = -1;
this._right = -1;
this._fontFamily = '';
this._fontWeight = '';
this._fontSize = -1;
this._fontFeatureSettings = '';
this._lineHeight = -1;
this._letterSpacing = -100;
this._className = '';
this._display = '';
this._position = '';
this._visibility = '';
this._backgroundColor = '';
this._layerHint = false;
this._contain = 'none';
this._boxShadow = '';
}
public setMaxWidth(maxWidth: number): void {
if (this._maxWidth === maxWidth) {
return;
}
this._maxWidth = maxWidth;
this.domNode.style.maxWidth = this._maxWidth + 'px';
}
public setWidth(width: number): void {
if (this._width === width) {
return;
}
this._width = width;
this.domNode.style.width = this._width + 'px';
}
public setHeight(height: number): void {
if (this._height === height) {
return;
}
this._height = height;
this.domNode.style.height = this._height + 'px';
}
public setTop(top: number): void {
if (this._top === top) {
return;
}
this._top = top;
this.domNode.style.top = this._top + 'px';
}
public unsetTop(): void {
if (this._top === -1) {
return;
}
this._top = -1;
this.domNode.style.top = '';
}
public setLeft(left: number): void {
if (this._left === left) {
return;
}
this._left = left;
this.domNode.style.left = this._left + 'px';
}
public setBottom(bottom: number): void {
if (this._bottom === bottom) {
return;
}
this._bottom = bottom;
this.domNode.style.bottom = this._bottom + 'px';
}
public setRight(right: number): void {
if (this._right === right) {
return;
}
this._right = right;
this.domNode.style.right = this._right + 'px';
}
public setFontFamily(fontFamily: string): void {
if (this._fontFamily === fontFamily) {
return;
}
this._fontFamily = fontFamily;
this.domNode.style.fontFamily = this._fontFamily;
}
public setFontWeight(fontWeight: string): void {
if (this._fontWeight === fontWeight) {
return;
}
this._fontWeight = fontWeight;
this.domNode.style.fontWeight = this._fontWeight;
}
public setFontSize(fontSize: number): void {
if (this._fontSize === fontSize) {
return;
}
this._fontSize = fontSize;
this.domNode.style.fontSize = this._fontSize + 'px';
}
public setFontFeatureSettings(fontFeatureSettings: string): void {
if (this._fontFeatureSettings === fontFeatureSettings) {
return;
}
this._fontFeatureSettings = fontFeatureSettings;
this.domNode.style.fontFeatureSettings = this._fontFeatureSettings;
}
public setLineHeight(lineHeight: number): void {
if (this._lineHeight === lineHeight) {
return;
}
this._lineHeight = lineHeight;
this.domNode.style.lineHeight = this._lineHeight + 'px';
}
public setLetterSpacing(letterSpacing: number): void {
if (this._letterSpacing === letterSpacing) {
return;
}
this._letterSpacing = letterSpacing;
this.domNode.style.letterSpacing = this._letterSpacing + 'px';
}
public setClassName(className: string): void {
if (this._className === className) {
return;
}
this._className = className;
this.domNode.className = this._className;
}
public toggleClassName(className: string, shouldHaveIt?: boolean): void {
this.domNode.classList.toggle(className, shouldHaveIt);
this._className = this.domNode.className;
}
public setDisplay(display: string): void {
if (this._display === display) {
return;
}
this._display = display;
this.domNode.style.display = this._display;
}
public setPosition(position: string): void {
if (this._position === position) {
return;
}
this._position = position;
this.domNode.style.position = this._position;
}
public setVisibility(visibility: string): void {
if (this._visibility === visibility) {
return;
}
this._visibility = visibility;
this.domNode.style.visibility = this._visibility;
}
public setBackgroundColor(backgroundColor: string): void {
if (this._backgroundColor === backgroundColor) {
return;
}
this._backgroundColor = backgroundColor;
this.domNode.style.backgroundColor = this._backgroundColor;
}
public setLayerHinting(layerHint: boolean): void {
if (this._layerHint === layerHint) {
return;
}
this._layerHint = layerHint;
this.domNode.style.transform = this._layerHint ? 'translate3d(0px, 0px, 0px)' : '';
}
public setBoxShadow(boxShadow: string): void {
if (this._boxShadow === boxShadow) {
return;
}
this._boxShadow = boxShadow;
this.domNode.style.boxShadow = boxShadow;
}
public setContain(contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint'): void {
if (this._contain === contain) {
return;
}
this._contain = contain;
(<any>this.domNode.style).contain = this._contain;
}
public setAttribute(name: string, value: string): void {
this.domNode.setAttribute(name, value);
}
public removeAttribute(name: string): void {
this.domNode.removeAttribute(name);
}
public appendChild(child: FastDomNode<T>): void {
this.domNode.appendChild(child.domNode);
}
public removeChild(child: FastDomNode<T>): void {
this.domNode.removeChild(child.domNode);
}
}
export function createFastDomNode<T extends HTMLElement>(domNode: T): FastDomNode<T> {
return new FastDomNode(domNode);
}

View File

@ -0,0 +1,220 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { DisposableStore } from 'vs/base/common/lifecycle';
export interface IContentActionHandler {
callback: (content: string, event?: IMouseEvent) => void;
readonly disposeables: DisposableStore;
}
export interface FormattedTextRenderOptions {
readonly className?: string;
readonly inline?: boolean;
readonly actionHandler?: IContentActionHandler;
}
export function renderText(text: string, options: FormattedTextRenderOptions = {}): HTMLElement {
const element = createElement(options);
element.textContent = text;
return element;
}
export function renderFormattedText(formattedText: string, options: FormattedTextRenderOptions = {}): HTMLElement {
const element = createElement(options);
_renderFormattedText(element, parseFormattedText(formattedText), options.actionHandler);
return element;
}
export function createElement(options: FormattedTextRenderOptions): HTMLElement {
const tagName = options.inline ? 'span' : 'div';
const element = document.createElement(tagName);
if (options.className) {
element.className = options.className;
}
return element;
}
class StringStream {
private source: string;
private index: number;
constructor(source: string) {
this.source = source;
this.index = 0;
}
public eos(): boolean {
return this.index >= this.source.length;
}
public next(): string {
const next = this.peek();
this.advance();
return next;
}
public peek(): string {
return this.source[this.index];
}
public advance(): void {
this.index++;
}
}
const enum FormatType {
Invalid,
Root,
Text,
Bold,
Italics,
Action,
ActionClose,
NewLine
}
interface IFormatParseTree {
type: FormatType;
content?: string;
index?: number;
children?: IFormatParseTree[];
}
function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler) {
let child: Node | undefined;
if (treeNode.type === FormatType.Text) {
child = document.createTextNode(treeNode.content || '');
} else if (treeNode.type === FormatType.Bold) {
child = document.createElement('b');
} else if (treeNode.type === FormatType.Italics) {
child = document.createElement('i');
} else if (treeNode.type === FormatType.Action && actionHandler) {
const a = document.createElement('a');
a.href = '#';
actionHandler.disposeables.add(DOM.addStandardDisposableListener(a, 'click', (event) => {
actionHandler.callback(String(treeNode.index), event);
}));
child = a;
} else if (treeNode.type === FormatType.NewLine) {
child = document.createElement('br');
} else if (treeNode.type === FormatType.Root) {
child = element;
}
if (child && element !== child) {
element.appendChild(child);
}
if (child && Array.isArray(treeNode.children)) {
treeNode.children.forEach((nodeChild) => {
_renderFormattedText(child!, nodeChild, actionHandler);
});
}
}
function parseFormattedText(content: string): IFormatParseTree {
const root: IFormatParseTree = {
type: FormatType.Root,
children: []
};
let actionViewItemIndex = 0;
let current = root;
const stack: IFormatParseTree[] = [];
const stream = new StringStream(content);
while (!stream.eos()) {
let next = stream.next();
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== FormatType.Invalid);
if (isEscapedFormatType) {
next = stream.next(); // unread the backslash if it escapes a format tag type
}
if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) {
stream.advance();
if (current.type === FormatType.Text) {
current = stack.pop()!;
}
const type = formatTagType(next);
if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) {
current = stack.pop()!;
} else {
const newCurrent: IFormatParseTree = {
type: type,
children: []
};
if (type === FormatType.Action) {
newCurrent.index = actionViewItemIndex;
actionViewItemIndex++;
}
current.children!.push(newCurrent);
stack.push(current);
current = newCurrent;
}
} else if (next === '\n') {
if (current.type === FormatType.Text) {
current = stack.pop()!;
}
current.children!.push({
type: FormatType.NewLine
});
} else {
if (current.type !== FormatType.Text) {
const textCurrent: IFormatParseTree = {
type: FormatType.Text,
content: next
};
current.children!.push(textCurrent);
stack.push(current);
current = textCurrent;
} else {
current.content += next;
}
}
}
if (current.type === FormatType.Text) {
current = stack.pop()!;
}
if (stack.length) {
// incorrectly formatted string literal
}
return root;
}
function isFormatTag(char: string): boolean {
return formatTagType(char) !== FormatType.Invalid;
}
function formatTagType(char: string): FormatType {
switch (char) {
case '*':
return FormatType.Bold;
case '_':
return FormatType.Italics;
case '[':
return FormatType.Action;
case ']':
return FormatType.ActionClose;
default:
return FormatType.Invalid;
}
}

View File

@ -0,0 +1,140 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import * as platform from 'vs/base/common/platform';
import { IframeUtils } from 'vs/base/browser/iframe';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { BrowserFeatures } from 'vs/base/browser/canIUse';
export interface IStandardMouseMoveEventData {
leftButton: boolean;
buttons: number;
posx: number;
posy: number;
}
export interface IEventMerger<R> {
(lastEvent: R | null, currentEvent: MouseEvent): R;
}
export interface IMouseMoveCallback<R> {
(mouseMoveData: R): void;
}
export interface IOnStopCallback {
(): void;
}
export function standardMouseMoveMerger(lastEvent: IStandardMouseMoveEventData | null, currentEvent: MouseEvent): IStandardMouseMoveEventData {
let ev = new StandardMouseEvent(currentEvent);
ev.preventDefault();
return {
leftButton: ev.leftButton,
buttons: ev.buttons,
posx: ev.posx,
posy: ev.posy
};
}
export class GlobalMouseMoveMonitor<R extends { buttons: number; }> implements IDisposable {
private readonly _hooks = new DisposableStore();
private _mouseMoveEventMerger: IEventMerger<R> | null = null;
private _mouseMoveCallback: IMouseMoveCallback<R> | null = null;
private _onStopCallback: IOnStopCallback | null = null;
public dispose(): void {
this.stopMonitoring(false);
this._hooks.dispose();
}
public stopMonitoring(invokeStopCallback: boolean): void {
if (!this.isMonitoring()) {
// Not monitoring
return;
}
// Unhook
this._hooks.clear();
this._mouseMoveEventMerger = null;
this._mouseMoveCallback = null;
const onStopCallback = this._onStopCallback;
this._onStopCallback = null;
if (invokeStopCallback && onStopCallback) {
onStopCallback();
}
}
public isMonitoring(): boolean {
return !!this._mouseMoveEventMerger;
}
public startMonitoring(
initialElement: HTMLElement,
initialButtons: number,
mouseMoveEventMerger: IEventMerger<R>,
mouseMoveCallback: IMouseMoveCallback<R>,
onStopCallback: IOnStopCallback
): void {
if (this.isMonitoring()) {
// I am already hooked
return;
}
this._mouseMoveEventMerger = mouseMoveEventMerger;
this._mouseMoveCallback = mouseMoveCallback;
this._onStopCallback = onStopCallback;
const windowChain = IframeUtils.getSameOriginWindowChain();
const mouseMove = platform.isIOS && BrowserFeatures.pointerEvents ? 'pointermove' : 'mousemove';
const mouseUp = platform.isIOS && BrowserFeatures.pointerEvents ? 'pointerup' : 'mouseup';
const listenTo: (Document | ShadowRoot)[] = windowChain.map(element => element.window.document);
const shadowRoot = dom.getShadowRoot(initialElement);
if (shadowRoot) {
listenTo.unshift(shadowRoot);
}
for (const element of listenTo) {
this._hooks.add(dom.addDisposableThrottledListener(element, mouseMove,
(data: R) => {
if (data.buttons !== initialButtons) {
// Buttons state has changed in the meantime
this.stopMonitoring(true);
return;
}
this._mouseMoveCallback!(data);
},
(lastEvent: R | null, currentEvent) => this._mouseMoveEventMerger!(lastEvent, currentEvent as MouseEvent)
));
this._hooks.add(dom.addDisposableListener(element, mouseUp, (e: MouseEvent) => this.stopMonitoring(true)));
}
if (IframeUtils.hasDifferentOriginAncestor()) {
let lastSameOriginAncestor = windowChain[windowChain.length - 1];
// We might miss a mouse up if it happens outside the iframe
// This one is for Chrome
this._hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document, 'mouseout', (browserEvent: MouseEvent) => {
let e = new StandardMouseEvent(browserEvent);
if (e.target.tagName.toLowerCase() === 'html') {
this.stopMonitoring(true);
}
}));
// This one is for FF
this._hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document, 'mouseover', (browserEvent: MouseEvent) => {
let e = new StandardMouseEvent(browserEvent);
if (e.target.tagName.toLowerCase() === 'html') {
this.stopMonitoring(true);
}
}));
// This one is for IE
this._hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document.body, 'mouseleave', (browserEvent: MouseEvent) => {
this.stopMonitoring(true);
}));
}
}
}

View File

@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface IHistoryNavigationWidget {
showPreviousValue(): void;
showNextValue(): void;
}

View File

@ -0,0 +1,127 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Represents a window in a possible chain of iframes
*/
export interface IWindowChainElement {
/**
* The window object for it
*/
window: Window;
/**
* The iframe element inside the window.parent corresponding to window
*/
iframeElement: Element | null;
}
let hasDifferentOriginAncestorFlag: boolean = false;
let sameOriginWindowChainCache: IWindowChainElement[] | null = null;
function getParentWindowIfSameOrigin(w: Window): Window | null {
if (!w.parent || w.parent === w) {
return null;
}
// Cannot really tell if we have access to the parent window unless we try to access something in it
try {
let location = w.location;
let parentLocation = w.parent.location;
if (location.origin !== 'null' && parentLocation.origin !== 'null') {
if (location.protocol !== parentLocation.protocol || location.hostname !== parentLocation.hostname || location.port !== parentLocation.port) {
hasDifferentOriginAncestorFlag = true;
return null;
}
}
} catch (e) {
hasDifferentOriginAncestorFlag = true;
return null;
}
return w.parent;
}
export class IframeUtils {
/**
* Returns a chain of embedded windows with the same origin (which can be accessed programmatically).
* Having a chain of length 1 might mean that the current execution environment is running outside of an iframe or inside an iframe embedded in a window with a different origin.
* To distinguish if at one point the current execution environment is running inside a window with a different origin, see hasDifferentOriginAncestor()
*/
public static getSameOriginWindowChain(): IWindowChainElement[] {
if (!sameOriginWindowChainCache) {
sameOriginWindowChainCache = [];
let w: Window | null = window;
let parent: Window | null;
do {
parent = getParentWindowIfSameOrigin(w);
if (parent) {
sameOriginWindowChainCache.push({
window: w,
iframeElement: w.frameElement || null
});
} else {
sameOriginWindowChainCache.push({
window: w,
iframeElement: null
});
}
w = parent;
} while (w);
}
return sameOriginWindowChainCache.slice(0);
}
/**
* Returns true if the current execution environment is chained in a list of iframes which at one point ends in a window with a different origin.
* Returns false if the current execution environment is not running inside an iframe or if the entire chain of iframes have the same origin.
*/
public static hasDifferentOriginAncestor(): boolean {
if (!sameOriginWindowChainCache) {
this.getSameOriginWindowChain();
}
return hasDifferentOriginAncestorFlag;
}
/**
* Returns the position of `childWindow` relative to `ancestorWindow`
*/
public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: Window | null) {
if (!ancestorWindow || childWindow === ancestorWindow) {
return {
top: 0,
left: 0
};
}
let top = 0, left = 0;
let windowChain = this.getSameOriginWindowChain();
for (const windowChainEl of windowChain) {
top += windowChainEl.window.scrollY;
left += windowChainEl.window.scrollX;
if (windowChainEl.window === ancestorWindow) {
break;
}
if (!windowChainEl.iframeElement) {
break;
}
let boundingRect = windowChainEl.iframeElement.getBoundingClientRect();
top += boundingRect.top;
left += boundingRect.left;
}
return {
top: top,
left: left
};
}
}

View File

@ -0,0 +1,336 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as browser from 'vs/base/browser/browser';
import { KeyCode, KeyCodeUtils, KeyMod, SimpleKeybinding } from 'vs/base/common/keyCodes';
import * as platform from 'vs/base/common/platform';
let KEY_CODE_MAP: { [keyCode: number]: KeyCode } = new Array(230);
let INVERSE_KEY_CODE_MAP: KeyCode[] = new Array(KeyCode.MAX_VALUE);
(function () {
for (let i = 0; i < INVERSE_KEY_CODE_MAP.length; i++) {
INVERSE_KEY_CODE_MAP[i] = -1;
}
function define(code: number, keyCode: KeyCode): void {
KEY_CODE_MAP[code] = keyCode;
INVERSE_KEY_CODE_MAP[keyCode] = code;
}
define(3, KeyCode.PauseBreak); // VK_CANCEL 0x03 Control-break processing
define(8, KeyCode.Backspace);
define(9, KeyCode.Tab);
define(13, KeyCode.Enter);
define(16, KeyCode.Shift);
define(17, KeyCode.Ctrl);
define(18, KeyCode.Alt);
define(19, KeyCode.PauseBreak);
define(20, KeyCode.CapsLock);
define(27, KeyCode.Escape);
define(32, KeyCode.Space);
define(33, KeyCode.PageUp);
define(34, KeyCode.PageDown);
define(35, KeyCode.End);
define(36, KeyCode.Home);
define(37, KeyCode.LeftArrow);
define(38, KeyCode.UpArrow);
define(39, KeyCode.RightArrow);
define(40, KeyCode.DownArrow);
define(45, KeyCode.Insert);
define(46, KeyCode.Delete);
define(48, KeyCode.KEY_0);
define(49, KeyCode.KEY_1);
define(50, KeyCode.KEY_2);
define(51, KeyCode.KEY_3);
define(52, KeyCode.KEY_4);
define(53, KeyCode.KEY_5);
define(54, KeyCode.KEY_6);
define(55, KeyCode.KEY_7);
define(56, KeyCode.KEY_8);
define(57, KeyCode.KEY_9);
define(65, KeyCode.KEY_A);
define(66, KeyCode.KEY_B);
define(67, KeyCode.KEY_C);
define(68, KeyCode.KEY_D);
define(69, KeyCode.KEY_E);
define(70, KeyCode.KEY_F);
define(71, KeyCode.KEY_G);
define(72, KeyCode.KEY_H);
define(73, KeyCode.KEY_I);
define(74, KeyCode.KEY_J);
define(75, KeyCode.KEY_K);
define(76, KeyCode.KEY_L);
define(77, KeyCode.KEY_M);
define(78, KeyCode.KEY_N);
define(79, KeyCode.KEY_O);
define(80, KeyCode.KEY_P);
define(81, KeyCode.KEY_Q);
define(82, KeyCode.KEY_R);
define(83, KeyCode.KEY_S);
define(84, KeyCode.KEY_T);
define(85, KeyCode.KEY_U);
define(86, KeyCode.KEY_V);
define(87, KeyCode.KEY_W);
define(88, KeyCode.KEY_X);
define(89, KeyCode.KEY_Y);
define(90, KeyCode.KEY_Z);
define(93, KeyCode.ContextMenu);
define(96, KeyCode.NUMPAD_0);
define(97, KeyCode.NUMPAD_1);
define(98, KeyCode.NUMPAD_2);
define(99, KeyCode.NUMPAD_3);
define(100, KeyCode.NUMPAD_4);
define(101, KeyCode.NUMPAD_5);
define(102, KeyCode.NUMPAD_6);
define(103, KeyCode.NUMPAD_7);
define(104, KeyCode.NUMPAD_8);
define(105, KeyCode.NUMPAD_9);
define(106, KeyCode.NUMPAD_MULTIPLY);
define(107, KeyCode.NUMPAD_ADD);
define(108, KeyCode.NUMPAD_SEPARATOR);
define(109, KeyCode.NUMPAD_SUBTRACT);
define(110, KeyCode.NUMPAD_DECIMAL);
define(111, KeyCode.NUMPAD_DIVIDE);
define(112, KeyCode.F1);
define(113, KeyCode.F2);
define(114, KeyCode.F3);
define(115, KeyCode.F4);
define(116, KeyCode.F5);
define(117, KeyCode.F6);
define(118, KeyCode.F7);
define(119, KeyCode.F8);
define(120, KeyCode.F9);
define(121, KeyCode.F10);
define(122, KeyCode.F11);
define(123, KeyCode.F12);
define(124, KeyCode.F13);
define(125, KeyCode.F14);
define(126, KeyCode.F15);
define(127, KeyCode.F16);
define(128, KeyCode.F17);
define(129, KeyCode.F18);
define(130, KeyCode.F19);
define(144, KeyCode.NumLock);
define(145, KeyCode.ScrollLock);
define(186, KeyCode.US_SEMICOLON);
define(187, KeyCode.US_EQUAL);
define(188, KeyCode.US_COMMA);
define(189, KeyCode.US_MINUS);
define(190, KeyCode.US_DOT);
define(191, KeyCode.US_SLASH);
define(192, KeyCode.US_BACKTICK);
define(193, KeyCode.ABNT_C1);
define(194, KeyCode.ABNT_C2);
define(219, KeyCode.US_OPEN_SQUARE_BRACKET);
define(220, KeyCode.US_BACKSLASH);
define(221, KeyCode.US_CLOSE_SQUARE_BRACKET);
define(222, KeyCode.US_QUOTE);
define(223, KeyCode.OEM_8);
define(226, KeyCode.OEM_102);
/**
* https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
* If an Input Method Editor is processing key input and the event is keydown, return 229.
*/
define(229, KeyCode.KEY_IN_COMPOSITION);
if (browser.isFirefox) {
define(59, KeyCode.US_SEMICOLON);
define(107, KeyCode.US_EQUAL);
define(109, KeyCode.US_MINUS);
if (platform.isMacintosh) {
define(224, KeyCode.Meta);
}
} else if (browser.isWebKit) {
define(91, KeyCode.Meta);
if (platform.isMacintosh) {
// the two meta keys in the Mac have different key codes (91 and 93)
define(93, KeyCode.Meta);
} else {
define(92, KeyCode.Meta);
}
}
})();
function extractKeyCode(e: KeyboardEvent): KeyCode {
if (e.charCode) {
// "keypress" events mostly
let char = String.fromCharCode(e.charCode).toUpperCase();
return KeyCodeUtils.fromString(char);
}
return KEY_CODE_MAP[e.keyCode] || KeyCode.Unknown;
}
export function getCodeForKeyCode(keyCode: KeyCode): number {
return INVERSE_KEY_CODE_MAP[keyCode];
}
export interface IKeyboardEvent {
readonly _standardKeyboardEventBrand: true;
readonly browserEvent: KeyboardEvent;
readonly target: HTMLElement;
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly keyCode: KeyCode;
readonly code: string;
/**
* @internal
*/
toKeybinding(): SimpleKeybinding;
equals(keybinding: number): boolean;
preventDefault(): void;
stopPropagation(): void;
}
const ctrlKeyMod = (platform.isMacintosh ? KeyMod.WinCtrl : KeyMod.CtrlCmd);
const altKeyMod = KeyMod.Alt;
const shiftKeyMod = KeyMod.Shift;
const metaKeyMod = (platform.isMacintosh ? KeyMod.CtrlCmd : KeyMod.WinCtrl);
export function printKeyboardEvent(e: KeyboardEvent): string {
let modifiers: string[] = [];
if (e.ctrlKey) {
modifiers.push(`ctrl`);
}
if (e.shiftKey) {
modifiers.push(`shift`);
}
if (e.altKey) {
modifiers.push(`alt`);
}
if (e.metaKey) {
modifiers.push(`meta`);
}
return `modifiers: [${modifiers.join(',')}], code: ${e.code}, keyCode: ${e.keyCode}, key: ${e.key}`;
}
export function printStandardKeyboardEvent(e: StandardKeyboardEvent): string {
let modifiers: string[] = [];
if (e.ctrlKey) {
modifiers.push(`ctrl`);
}
if (e.shiftKey) {
modifiers.push(`shift`);
}
if (e.altKey) {
modifiers.push(`alt`);
}
if (e.metaKey) {
modifiers.push(`meta`);
}
return `modifiers: [${modifiers.join(',')}], code: ${e.code}, keyCode: ${e.keyCode} ('${KeyCodeUtils.toString(e.keyCode)}')`;
}
export class StandardKeyboardEvent implements IKeyboardEvent {
readonly _standardKeyboardEventBrand = true;
public readonly browserEvent: KeyboardEvent;
public readonly target: HTMLElement;
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly keyCode: KeyCode;
public readonly code: string;
private _asKeybinding: number;
private _asRuntimeKeybinding: SimpleKeybinding;
constructor(source: KeyboardEvent) {
let e = source;
this.browserEvent = e;
this.target = <HTMLElement>e.target;
this.ctrlKey = e.ctrlKey;
this.shiftKey = e.shiftKey;
this.altKey = e.altKey;
this.metaKey = e.metaKey;
this.keyCode = extractKeyCode(e);
this.code = e.code;
// console.info(e.type + ": keyCode: " + e.keyCode + ", which: " + e.which + ", charCode: " + e.charCode + ", detail: " + e.detail + " ====> " + this.keyCode + ' -- ' + KeyCode[this.keyCode]);
this.ctrlKey = this.ctrlKey || this.keyCode === KeyCode.Ctrl;
this.altKey = this.altKey || this.keyCode === KeyCode.Alt;
this.shiftKey = this.shiftKey || this.keyCode === KeyCode.Shift;
this.metaKey = this.metaKey || this.keyCode === KeyCode.Meta;
this._asKeybinding = this._computeKeybinding();
this._asRuntimeKeybinding = this._computeRuntimeKeybinding();
// console.log(`code: ${e.code}, keyCode: ${e.keyCode}, key: ${e.key}`);
}
public preventDefault(): void {
if (this.browserEvent && this.browserEvent.preventDefault) {
this.browserEvent.preventDefault();
}
}
public stopPropagation(): void {
if (this.browserEvent && this.browserEvent.stopPropagation) {
this.browserEvent.stopPropagation();
}
}
public toKeybinding(): SimpleKeybinding {
return this._asRuntimeKeybinding;
}
public equals(other: number): boolean {
return this._asKeybinding === other;
}
private _computeKeybinding(): number {
let key = KeyCode.Unknown;
if (this.keyCode !== KeyCode.Ctrl && this.keyCode !== KeyCode.Shift && this.keyCode !== KeyCode.Alt && this.keyCode !== KeyCode.Meta) {
key = this.keyCode;
}
let result = 0;
if (this.ctrlKey) {
result |= ctrlKeyMod;
}
if (this.altKey) {
result |= altKeyMod;
}
if (this.shiftKey) {
result |= shiftKeyMod;
}
if (this.metaKey) {
result |= metaKeyMod;
}
result |= key;
return result;
}
private _computeRuntimeKeybinding(): SimpleKeybinding {
let key = KeyCode.Unknown;
if (this.keyCode !== KeyCode.Ctrl && this.keyCode !== KeyCode.Shift && this.keyCode !== KeyCode.Alt && this.keyCode !== KeyCode.Meta) {
key = this.keyCode;
}
return new SimpleKeybinding(this.ctrlKey, this.shiftKey, this.altKey, this.metaKey, key);
}
}

View File

@ -0,0 +1,303 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IMarkdownString, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
import { defaultGenerator } from 'vs/base/common/idGenerator';
import * as marked from 'vs/base/common/marked/marked';
import { insane, InsaneOptions } from 'vs/base/common/insane/insane';
import { parse } from 'vs/base/common/marshalling';
import { cloneAndChange } from 'vs/base/common/objects';
import { escape } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { FileAccess, Schemas } from 'vs/base/common/network';
import { markdownEscapeEscapedCodicons } from 'vs/base/common/codicons';
import { resolvePath } from 'vs/base/common/resources';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { renderCodicons } from 'vs/base/browser/codicons';
import { Event } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
export interface MarkedOptions extends marked.MarkedOptions {
baseUrl?: never;
}
export interface MarkdownRenderOptions extends FormattedTextRenderOptions {
codeBlockRenderer?: (modeId: string, value: string) => Promise<HTMLElement>;
codeBlockRenderCallback?: () => void;
baseUrl?: URI;
}
const _ttpInsane = window.trustedTypes?.createPolicy('insane', {
createHTML(value, options: InsaneOptions): string {
return insane(value, options);
}
});
/**
* Low-level way create a html element from a markdown string.
*
* **Note** that for most cases you should be using [`MarkdownRenderer`](./src/vs/editor/browser/core/markdownRenderer.ts)
* which comes with support for pretty code block rendering and which uses the default way of handling links.
*/
export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: MarkedOptions = {}): HTMLElement {
const element = createElement(options);
const _uriMassage = function (part: string): string {
let data: any;
try {
data = parse(decodeURIComponent(part));
} catch (e) {
// ignore
}
if (!data) {
return part;
}
data = cloneAndChange(data, value => {
if (markdown.uris && markdown.uris[value]) {
return URI.revive(markdown.uris[value]);
} else {
return undefined;
}
});
return encodeURIComponent(JSON.stringify(data));
};
const _href = function (href: string, isDomUri: boolean): string {
const data = markdown.uris && markdown.uris[href];
if (!data) {
return href; // no uri exists
}
let uri = URI.revive(data);
if (URI.parse(href).toString() === uri.toString()) {
return href; // no tranformation performed
}
if (isDomUri) {
// this URI will end up as "src"-attribute of a dom node
// and because of that special rewriting needs to be done
// so that the URI uses a protocol that's understood by
// browsers (like http or https)
return FileAccess.asBrowserUri(uri).toString(true);
}
if (uri.query) {
uri = uri.with({ query: _uriMassage(uri.query) });
}
return uri.toString();
};
// signal to code-block render that the
// element has been created
let signalInnerHTML: () => void;
const withInnerHTML = new Promise<void>(c => signalInnerHTML = c);
const renderer = new marked.Renderer();
renderer.image = (href: string, title: string, text: string) => {
let dimensions: string[] = [];
let attributes: string[] = [];
if (href) {
({ href, dimensions } = parseHrefAndDimensions(href));
href = _href(href, true);
try {
const hrefAsUri = URI.parse(href);
if (options.baseUrl && hrefAsUri.scheme === Schemas.file) { // absolute or relative local path, or file: uri
href = resolvePath(options.baseUrl, href).toString();
}
} catch (err) { }
attributes.push(`src="${href}"`);
}
if (text) {
attributes.push(`alt="${text}"`);
}
if (title) {
attributes.push(`title="${title}"`);
}
if (dimensions.length) {
attributes = attributes.concat(dimensions);
}
return '<img ' + attributes.join(' ') + '>';
};
renderer.link = (href, title, text): string => {
// Remove markdown escapes. Workaround for https://github.com/chjj/marked/issues/829
if (href === text) { // raw link case
text = removeMarkdownEscapes(text);
}
href = _href(href, false);
if (options.baseUrl) {
const hasScheme = /^\w[\w\d+.-]*:/.test(href);
if (!hasScheme) {
href = resolvePath(options.baseUrl, href).toString();
}
}
title = removeMarkdownEscapes(title);
href = removeMarkdownEscapes(href);
if (
!href
|| href.match(/^data:|javascript:/i)
|| (href.match(/^command:/i) && !markdown.isTrusted)
|| href.match(/^command:(\/\/\/)?_workbench\.downloadResource/i)
) {
// drop the link
return text;
} else {
// HTML Encode href
href = href.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
return `<a href="#" data-href="${href}" title="${title || href}">${text}</a>`;
}
};
renderer.paragraph = (text): string => {
if (markdown.supportThemeIcons) {
const elements = renderCodicons(text);
text = elements.map(e => typeof e === 'string' ? e : e.outerHTML).join('');
}
return `<p>${text}</p>`;
};
if (options.codeBlockRenderer) {
renderer.code = (code, lang) => {
const value = options.codeBlockRenderer!(lang, code);
// when code-block rendering is async we return sync
// but update the node with the real result later.
const id = defaultGenerator.nextId();
const promise = Promise.all([value, withInnerHTML]).then(values => {
const span = <HTMLDivElement>element.querySelector(`div[data-code="${id}"]`);
if (span) {
DOM.reset(span, values[0]);
}
}).catch(_err => {
// ignore
});
if (options.codeBlockRenderCallback) {
promise.then(options.codeBlockRenderCallback);
}
return `<div class="code" data-code="${id}">${escape(code)}</div>`;
};
}
if (options.actionHandler) {
options.actionHandler.disposeables.add(Event.any<MouseEvent>(domEvent(element, 'click'), domEvent(element, 'auxclick'))(e => {
const mouseEvent = new StandardMouseEvent(e);
if (!mouseEvent.leftButton && !mouseEvent.middleButton) {
return;
}
let target: HTMLElement | null = mouseEvent.target;
if (target.tagName !== 'A') {
target = target.parentElement;
if (!target || target.tagName !== 'A') {
return;
}
}
try {
const href = target.dataset['href'];
if (href) {
options.actionHandler!.callback(href, mouseEvent);
}
} catch (err) {
onUnexpectedError(err);
} finally {
mouseEvent.preventDefault();
}
}));
}
// Use our own sanitizer so that we can let through only spans.
// Otherwise, we'd be letting all html be rendered.
// If we want to allow markdown permitted tags, then we can delete sanitizer and sanitize.
markedOptions.sanitizer = (html: string): string => {
const match = markdown.isTrusted ? html.match(/^(<span[^<]+>)|(<\/\s*span>)$/) : undefined;
return match ? html : '';
};
markedOptions.sanitize = true;
markedOptions.renderer = renderer;
// values that are too long will freeze the UI
let value = markdown.value ?? '';
if (value.length > 100_000) {
value = `${value.substr(0, 100_000)}`;
}
// escape theme icons
if (markdown.supportThemeIcons) {
value = markdownEscapeEscapedCodicons(value);
}
const renderedMarkdown = marked.parse(value, markedOptions);
// sanitize with insane
element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown);
// signal that async code blocks can be now be inserted
signalInnerHTML!();
return element;
}
function sanitizeRenderedMarkdown(
options: { isTrusted?: boolean },
renderedMarkdown: string,
): string {
const insaneOptions = getInsaneOptions(options);
if (_ttpInsane) {
return _ttpInsane.createHTML(renderedMarkdown, insaneOptions) as unknown as string;
} else {
return insane(renderedMarkdown, insaneOptions);
}
}
function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOptions {
const allowedSchemes = [
Schemas.http,
Schemas.https,
Schemas.mailto,
Schemas.data,
Schemas.file,
Schemas.vscodeRemote,
Schemas.vscodeRemoteResource,
];
if (options.isTrusted) {
allowedSchemes.push(Schemas.command);
}
return {
allowedSchemes,
// allowedTags should included everything that markdown renders to.
// Since we have our own sanitize function for marked, it's possible we missed some tag so let insane make sure.
// HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/
// HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension-
allowedTags: ['ul', 'li', 'p', 'code', 'blockquote', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'em', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'div', 'del', 'a', 'strong', 'br', 'img', 'span'],
allowedAttributes: {
'a': ['href', 'name', 'target', 'data-href'],
'img': ['src', 'title', 'alt', 'width', 'height'],
'div': ['class', 'data-code'],
'span': ['class', 'style'],
// https://github.com/microsoft/vscode/issues/95937
'th': ['align'],
'td': ['align']
},
filter(token: { tag: string; attrs: { readonly [key: string]: string; }; }): boolean {
if (token.tag === 'span' && options.isTrusted && (Object.keys(token.attrs).length === 1)) {
if (token.attrs['style']) {
return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/);
} else if (token.attrs['class']) {
// The class should match codicon rendering in src\vs\base\common\codicons.ts
return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-animation-[a-z\-]+)?$/);
}
return false;
}
return true;
}
};
}

View File

@ -0,0 +1,224 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as browser from 'vs/base/browser/browser';
import { IframeUtils } from 'vs/base/browser/iframe';
import * as platform from 'vs/base/common/platform';
export interface IMouseEvent {
readonly browserEvent: MouseEvent;
readonly leftButton: boolean;
readonly middleButton: boolean;
readonly rightButton: boolean;
readonly buttons: number;
readonly target: HTMLElement;
readonly detail: number;
readonly posx: number;
readonly posy: number;
readonly ctrlKey: boolean;
readonly shiftKey: boolean;
readonly altKey: boolean;
readonly metaKey: boolean;
readonly timestamp: number;
preventDefault(): void;
stopPropagation(): void;
}
export class StandardMouseEvent implements IMouseEvent {
public readonly browserEvent: MouseEvent;
public readonly leftButton: boolean;
public readonly middleButton: boolean;
public readonly rightButton: boolean;
public readonly buttons: number;
public readonly target: HTMLElement;
public detail: number;
public readonly posx: number;
public readonly posy: number;
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly timestamp: number;
constructor(e: MouseEvent) {
this.timestamp = Date.now();
this.browserEvent = e;
this.leftButton = e.button === 0;
this.middleButton = e.button === 1;
this.rightButton = e.button === 2;
this.buttons = e.buttons;
this.target = <HTMLElement>e.target;
this.detail = e.detail || 1;
if (e.type === 'dblclick') {
this.detail = 2;
}
this.ctrlKey = e.ctrlKey;
this.shiftKey = e.shiftKey;
this.altKey = e.altKey;
this.metaKey = e.metaKey;
if (typeof e.pageX === 'number') {
this.posx = e.pageX;
this.posy = e.pageY;
} else {
// Probably hit by MSGestureEvent
this.posx = e.clientX + document.body.scrollLeft + document.documentElement!.scrollLeft;
this.posy = e.clientY + document.body.scrollTop + document.documentElement!.scrollTop;
}
// Find the position of the iframe this code is executing in relative to the iframe where the event was captured.
let iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow(self, e.view);
this.posx -= iframeOffsets.left;
this.posy -= iframeOffsets.top;
}
public preventDefault(): void {
this.browserEvent.preventDefault();
}
public stopPropagation(): void {
this.browserEvent.stopPropagation();
}
}
export interface IDataTransfer {
dropEffect: string;
effectAllowed: string;
types: any[];
files: any[];
setData(type: string, data: string): void;
setDragImage(image: any, x: number, y: number): void;
getData(type: string): string;
clearData(types?: string[]): void;
}
export class DragMouseEvent extends StandardMouseEvent {
public readonly dataTransfer: IDataTransfer;
constructor(e: MouseEvent) {
super(e);
this.dataTransfer = (<any>e).dataTransfer;
}
}
export interface IMouseWheelEvent extends MouseEvent {
readonly wheelDelta: number;
readonly wheelDeltaX: number;
readonly wheelDeltaY: number;
readonly deltaX: number;
readonly deltaY: number;
readonly deltaZ: number;
readonly deltaMode: number;
}
interface IWebKitMouseWheelEvent {
wheelDeltaY: number;
wheelDeltaX: number;
}
interface IGeckoMouseWheelEvent {
HORIZONTAL_AXIS: number;
VERTICAL_AXIS: number;
axis: number;
detail: number;
}
export class StandardWheelEvent {
public readonly browserEvent: IMouseWheelEvent | null;
public readonly deltaY: number;
public readonly deltaX: number;
public readonly target: Node;
constructor(e: IMouseWheelEvent | null, deltaX: number = 0, deltaY: number = 0) {
this.browserEvent = e || null;
this.target = e ? (e.target || (<any>e).targetNode || e.srcElement) : null;
this.deltaY = deltaY;
this.deltaX = deltaX;
if (e) {
// Old (deprecated) wheel events
let e1 = <IWebKitMouseWheelEvent><any>e;
let e2 = <IGeckoMouseWheelEvent><any>e;
// vertical delta scroll
if (typeof e1.wheelDeltaY !== 'undefined') {
this.deltaY = e1.wheelDeltaY / 120;
} else if (typeof e2.VERTICAL_AXIS !== 'undefined' && e2.axis === e2.VERTICAL_AXIS) {
this.deltaY = -e2.detail / 3;
} else if (e.type === 'wheel') {
// Modern wheel event
// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
const ev = <WheelEvent><unknown>e;
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
// the deltas are expressed in lines
if (browser.isFirefox && !platform.isMacintosh) {
this.deltaY = -e.deltaY / 3;
} else {
this.deltaY = -e.deltaY;
}
} else {
this.deltaY = -e.deltaY / 40;
}
}
// horizontal delta scroll
if (typeof e1.wheelDeltaX !== 'undefined') {
if (browser.isSafari && platform.isWindows) {
this.deltaX = - (e1.wheelDeltaX / 120);
} else {
this.deltaX = e1.wheelDeltaX / 120;
}
} else if (typeof e2.HORIZONTAL_AXIS !== 'undefined' && e2.axis === e2.HORIZONTAL_AXIS) {
this.deltaX = -e.detail / 3;
} else if (e.type === 'wheel') {
// Modern wheel event
// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
const ev = <WheelEvent><unknown>e;
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
// the deltas are expressed in lines
if (browser.isFirefox && !platform.isMacintosh) {
this.deltaX = -e.deltaX / 3;
} else {
this.deltaX = -e.deltaX;
}
} else {
this.deltaX = -e.deltaX / 40;
}
}
// Assume a vertical scroll if nothing else worked
if (this.deltaY === 0 && this.deltaX === 0 && e.wheelDelta) {
this.deltaY = e.wheelDelta / 120;
}
}
}
public preventDefault(): void {
if (this.browserEvent) {
this.browserEvent.preventDefault();
}
}
public stopPropagation(): void {
if (this.browserEvent) {
this.browserEvent.stopPropagation();
}
}
}

View File

@ -0,0 +1,363 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as arrays from 'vs/base/common/arrays';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import * as DomUtils from 'vs/base/browser/dom';
import { memoize } from 'vs/base/common/decorators';
export namespace EventType {
export const Tap = '-monaco-gesturetap';
export const Change = '-monaco-gesturechange';
export const Start = '-monaco-gesturestart';
export const End = '-monaco-gesturesend';
export const Contextmenu = '-monaco-gesturecontextmenu';
}
interface TouchData {
id: number;
initialTarget: EventTarget;
initialTimeStamp: number;
initialPageX: number;
initialPageY: number;
rollingTimestamps: number[];
rollingPageX: number[];
rollingPageY: number[];
}
export interface GestureEvent extends MouseEvent {
initialTarget: EventTarget | undefined;
translationX: number;
translationY: number;
pageX: number;
pageY: number;
tapCount: number;
}
interface Touch {
identifier: number;
screenX: number;
screenY: number;
clientX: number;
clientY: number;
pageX: number;
pageY: number;
radiusX: number;
radiusY: number;
rotationAngle: number;
force: number;
target: Element;
}
interface TouchList {
[i: number]: Touch;
length: number;
item(index: number): Touch;
identifiedTouch(id: number): Touch;
}
interface TouchEvent extends Event {
touches: TouchList;
targetTouches: TouchList;
changedTouches: TouchList;
}
export class Gesture extends Disposable {
private static readonly SCROLL_FRICTION = -0.005;
private static INSTANCE: Gesture;
private static readonly HOLD_DELAY = 700;
private dispatched = false;
private targets: HTMLElement[];
private ignoreTargets: HTMLElement[];
private handle: IDisposable | null;
private activeTouches: { [id: number]: TouchData; };
private _lastSetTapCountTime: number;
private static readonly CLEAR_TAP_COUNT_TIME = 400; // ms
private constructor() {
super();
this.activeTouches = {};
this.handle = null;
this.targets = [];
this.ignoreTargets = [];
this._lastSetTapCountTime = 0;
this._register(DomUtils.addDisposableListener(document, 'touchstart', (e: TouchEvent) => this.onTouchStart(e), { passive: false }));
this._register(DomUtils.addDisposableListener(document, 'touchend', (e: TouchEvent) => this.onTouchEnd(e)));
this._register(DomUtils.addDisposableListener(document, 'touchmove', (e: TouchEvent) => this.onTouchMove(e), { passive: false }));
}
public static addTarget(element: HTMLElement): IDisposable {
if (!Gesture.isTouchDevice()) {
return Disposable.None;
}
if (!Gesture.INSTANCE) {
Gesture.INSTANCE = new Gesture();
}
Gesture.INSTANCE.targets.push(element);
return {
dispose: () => {
Gesture.INSTANCE.targets = Gesture.INSTANCE.targets.filter(t => t !== element);
}
};
}
public static ignoreTarget(element: HTMLElement): IDisposable {
if (!Gesture.isTouchDevice()) {
return Disposable.None;
}
if (!Gesture.INSTANCE) {
Gesture.INSTANCE = new Gesture();
}
Gesture.INSTANCE.ignoreTargets.push(element);
return {
dispose: () => {
Gesture.INSTANCE.ignoreTargets = Gesture.INSTANCE.ignoreTargets.filter(t => t !== element);
}
};
}
@memoize
private static isTouchDevice(): boolean {
// `'ontouchstart' in window` always evaluates to true with typescript's modern typings. This causes `window` to be
// `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast
return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || (window as Window).navigator.msMaxTouchPoints > 0;
}
public dispose(): void {
if (this.handle) {
this.handle.dispose();
this.handle = null;
}
super.dispose();
}
private onTouchStart(e: TouchEvent): void {
let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
if (this.handle) {
this.handle.dispose();
this.handle = null;
}
for (let i = 0, len = e.targetTouches.length; i < len; i++) {
let touch = e.targetTouches.item(i);
this.activeTouches[touch.identifier] = {
id: touch.identifier,
initialTarget: touch.target,
initialTimeStamp: timestamp,
initialPageX: touch.pageX,
initialPageY: touch.pageY,
rollingTimestamps: [timestamp],
rollingPageX: [touch.pageX],
rollingPageY: [touch.pageY]
};
let evt = this.newGestureEvent(EventType.Start, touch.target);
evt.pageX = touch.pageX;
evt.pageY = touch.pageY;
this.dispatchEvent(evt);
}
if (this.dispatched) {
e.preventDefault();
e.stopPropagation();
this.dispatched = false;
}
}
private onTouchEnd(e: TouchEvent): void {
let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
let activeTouchCount = Object.keys(this.activeTouches).length;
for (let i = 0, len = e.changedTouches.length; i < len; i++) {
let touch = e.changedTouches.item(i);
if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
console.warn('move of an UNKNOWN touch', touch);
continue;
}
let data = this.activeTouches[touch.identifier],
holdTime = Date.now() - data.initialTimeStamp;
if (holdTime < Gesture.HOLD_DELAY
&& Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30
&& Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) {
let evt = this.newGestureEvent(EventType.Tap, data.initialTarget);
evt.pageX = arrays.tail(data.rollingPageX);
evt.pageY = arrays.tail(data.rollingPageY);
this.dispatchEvent(evt);
} else if (holdTime >= Gesture.HOLD_DELAY
&& Math.abs(data.initialPageX - arrays.tail(data.rollingPageX)) < 30
&& Math.abs(data.initialPageY - arrays.tail(data.rollingPageY)) < 30) {
let evt = this.newGestureEvent(EventType.Contextmenu, data.initialTarget);
evt.pageX = arrays.tail(data.rollingPageX);
evt.pageY = arrays.tail(data.rollingPageY);
this.dispatchEvent(evt);
} else if (activeTouchCount === 1) {
let finalX = arrays.tail(data.rollingPageX);
let finalY = arrays.tail(data.rollingPageY);
let deltaT = arrays.tail(data.rollingTimestamps) - data.rollingTimestamps[0];
let deltaX = finalX - data.rollingPageX[0];
let deltaY = finalY - data.rollingPageY[0];
// We need to get all the dispatch targets on the start of the inertia event
const dispatchTo = this.targets.filter(t => data.initialTarget instanceof Node && t.contains(data.initialTarget));
this.inertia(dispatchTo, timestamp, // time now
Math.abs(deltaX) / deltaT, // speed
deltaX > 0 ? 1 : -1, // x direction
finalX, // x now
Math.abs(deltaY) / deltaT, // y speed
deltaY > 0 ? 1 : -1, // y direction
finalY // y now
);
}
this.dispatchEvent(this.newGestureEvent(EventType.End, data.initialTarget));
// forget about this touch
delete this.activeTouches[touch.identifier];
}
if (this.dispatched) {
e.preventDefault();
e.stopPropagation();
this.dispatched = false;
}
}
private newGestureEvent(type: string, initialTarget?: EventTarget): GestureEvent {
let event = document.createEvent('CustomEvent') as unknown as GestureEvent;
event.initEvent(type, false, true);
event.initialTarget = initialTarget;
event.tapCount = 0;
return event;
}
private dispatchEvent(event: GestureEvent): void {
if (event.type === EventType.Tap) {
const currentTime = (new Date()).getTime();
let setTapCount = 0;
if (currentTime - this._lastSetTapCountTime > Gesture.CLEAR_TAP_COUNT_TIME) {
setTapCount = 1;
} else {
setTapCount = 2;
}
this._lastSetTapCountTime = currentTime;
event.tapCount = setTapCount;
} else if (event.type === EventType.Change || event.type === EventType.Contextmenu) {
// tap is canceled by scrolling or context menu
this._lastSetTapCountTime = 0;
}
for (let i = 0; i < this.ignoreTargets.length; i++) {
if (event.initialTarget instanceof Node && this.ignoreTargets[i].contains(event.initialTarget)) {
return;
}
}
this.targets.forEach(target => {
if (event.initialTarget instanceof Node && target.contains(event.initialTarget)) {
target.dispatchEvent(event);
this.dispatched = true;
}
});
}
private inertia(dispatchTo: EventTarget[], t1: number, vX: number, dirX: number, x: number, vY: number, dirY: number, y: number): void {
this.handle = DomUtils.scheduleAtNextAnimationFrame(() => {
let now = Date.now();
// velocity: old speed + accel_over_time
let deltaT = now - t1,
delta_pos_x = 0, delta_pos_y = 0,
stopped = true;
vX += Gesture.SCROLL_FRICTION * deltaT;
vY += Gesture.SCROLL_FRICTION * deltaT;
if (vX > 0) {
stopped = false;
delta_pos_x = dirX * vX * deltaT;
}
if (vY > 0) {
stopped = false;
delta_pos_y = dirY * vY * deltaT;
}
// dispatch translation event
let evt = this.newGestureEvent(EventType.Change);
evt.translationX = delta_pos_x;
evt.translationY = delta_pos_y;
dispatchTo.forEach(d => d.dispatchEvent(evt));
if (!stopped) {
this.inertia(dispatchTo, now, vX, dirX, x + delta_pos_x, vY, dirY, y + delta_pos_y);
}
});
}
private onTouchMove(e: TouchEvent): void {
let timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
for (let i = 0, len = e.changedTouches.length; i < len; i++) {
let touch = e.changedTouches.item(i);
if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
console.warn('end of an UNKNOWN touch', touch);
continue;
}
let data = this.activeTouches[touch.identifier];
let evt = this.newGestureEvent(EventType.Change, data.initialTarget);
evt.translationX = touch.pageX - arrays.tail(data.rollingPageX);
evt.translationY = touch.pageY - arrays.tail(data.rollingPageY);
evt.pageX = touch.pageX;
evt.pageY = touch.pageY;
this.dispatchEvent(evt);
// only keep a few data points, to average the final speed
if (data.rollingPageX.length > 3) {
data.rollingPageX.shift();
data.rollingPageY.shift();
data.rollingTimestamps.shift();
}
data.rollingPageX.push(touch.pageX);
data.rollingPageY.push(touch.pageY);
data.rollingTimestamps.push(timestamp);
}
if (this.dispatched) {
e.preventDefault();
e.stopPropagation();
this.dispatched = false;
}
}
}

View File

@ -0,0 +1,398 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./actionbar';
import * as platform from 'vs/base/common/platform';
import * as nls from 'vs/nls';
import { Disposable } from 'vs/base/common/lifecycle';
import { SelectBox, ISelectOptionItem, ISelectBoxOptions } from 'vs/base/browser/ui/selectBox/selectBox';
import { IAction, IActionRunner, Action, IActionChangeEvent, ActionRunner, Separator, IActionViewItem } from 'vs/base/common/actions';
import * as types from 'vs/base/common/types';
import { EventType as TouchEventType, Gesture } from 'vs/base/browser/touch';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { DataTransfers } from 'vs/base/browser/dnd';
import { isFirefox } from 'vs/base/browser/browser';
import { $, addDisposableListener, append, EventHelper, EventLike, EventType, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom';
export interface IBaseActionViewItemOptions {
draggable?: boolean;
isMenu?: boolean;
useEventAsContext?: boolean;
}
export class BaseActionViewItem extends Disposable implements IActionViewItem {
element: HTMLElement | undefined;
_context: any;
_action: IAction;
private _actionRunner: IActionRunner | undefined;
constructor(context: any, action: IAction, protected options: IBaseActionViewItemOptions = {}) {
super();
this._context = context || this;
this._action = action;
if (action instanceof Action) {
this._register(action.onDidChange(event => {
if (!this.element) {
// we have not been rendered yet, so there
// is no point in updating the UI
return;
}
this.handleActionChangeEvent(event);
}));
}
}
private handleActionChangeEvent(event: IActionChangeEvent): void {
if (event.enabled !== undefined) {
this.updateEnabled();
}
if (event.checked !== undefined) {
this.updateChecked();
}
if (event.class !== undefined) {
this.updateClass();
}
if (event.label !== undefined) {
this.updateLabel();
this.updateTooltip();
}
if (event.tooltip !== undefined) {
this.updateTooltip();
}
}
get actionRunner(): IActionRunner {
if (!this._actionRunner) {
this._actionRunner = this._register(new ActionRunner());
}
return this._actionRunner;
}
set actionRunner(actionRunner: IActionRunner) {
this._actionRunner = actionRunner;
}
getAction(): IAction {
return this._action;
}
isEnabled(): boolean {
return this._action.enabled;
}
setActionContext(newContext: unknown): void {
this._context = newContext;
}
render(container: HTMLElement): void {
const element = this.element = container;
this._register(Gesture.addTarget(container));
const enableDragging = this.options && this.options.draggable;
if (enableDragging) {
container.draggable = true;
if (isFirefox) {
// Firefox: requires to set a text data transfer to get going
this._register(addDisposableListener(container, EventType.DRAG_START, e => e.dataTransfer?.setData(DataTransfers.TEXT, this._action.label)));
}
}
this._register(addDisposableListener(element, TouchEventType.Tap, e => this.onClick(e)));
this._register(addDisposableListener(element, EventType.MOUSE_DOWN, e => {
if (!enableDragging) {
EventHelper.stop(e, true); // do not run when dragging is on because that would disable it
}
if (this._action.enabled && e.button === 0) {
element.classList.add('active');
}
}));
if (platform.isMacintosh) {
// macOS: allow to trigger the button when holding Ctrl+key and pressing the
// main mouse button. This is for scenarios where e.g. some interaction forces
// the Ctrl+key to be pressed and hold but the user still wants to interact
// with the actions (for example quick access in quick navigation mode).
this._register(addDisposableListener(element, EventType.CONTEXT_MENU, e => {
if (e.button === 0 && e.ctrlKey === true) {
this.onClick(e);
}
}));
}
this._register(addDisposableListener(element, EventType.CLICK, e => {
EventHelper.stop(e, true);
// menus do not use the click event
if (!(this.options && this.options.isMenu)) {
platform.setImmediate(() => this.onClick(e));
}
}));
this._register(addDisposableListener(element, EventType.DBLCLICK, e => {
EventHelper.stop(e, true);
}));
[EventType.MOUSE_UP, EventType.MOUSE_OUT].forEach(event => {
this._register(addDisposableListener(element, event, e => {
EventHelper.stop(e);
element.classList.remove('active');
}));
});
}
onClick(event: EventLike): void {
EventHelper.stop(event, true);
const context = types.isUndefinedOrNull(this._context) ? this.options?.useEventAsContext ? event : undefined : this._context;
this.actionRunner.run(this._action, context);
}
focus(): void {
if (this.element) {
this.element.focus();
this.element.classList.add('focused');
}
}
blur(): void {
if (this.element) {
this.element.blur();
this.element.classList.remove('focused');
}
}
protected updateEnabled(): void {
// implement in subclass
}
protected updateLabel(): void {
// implement in subclass
}
protected updateTooltip(): void {
// implement in subclass
}
protected updateClass(): void {
// implement in subclass
}
protected updateChecked(): void {
// implement in subclass
}
dispose(): void {
if (this.element) {
this.element.remove();
this.element = undefined;
}
super.dispose();
}
}
export interface IActionViewItemOptions extends IBaseActionViewItemOptions {
icon?: boolean;
label?: boolean;
keybinding?: string | null;
}
export class ActionViewItem extends BaseActionViewItem {
protected label: HTMLElement | undefined;
protected options: IActionViewItemOptions;
private cssClass?: string;
constructor(context: unknown, action: IAction, options: IActionViewItemOptions = {}) {
super(context, action, options);
this.options = options;
this.options.icon = options.icon !== undefined ? options.icon : false;
this.options.label = options.label !== undefined ? options.label : true;
this.cssClass = '';
}
render(container: HTMLElement): void {
super.render(container);
if (this.element) {
this.label = append(this.element, $('a.action-label'));
}
if (this.label) {
if (this._action.id === Separator.ID) {
this.label.setAttribute('role', 'presentation'); // A separator is a presentation item
} else {
if (this.options.isMenu) {
this.label.setAttribute('role', 'menuitem');
} else {
this.label.setAttribute('role', 'button');
}
}
}
if (this.options.label && this.options.keybinding && this.element) {
append(this.element, $('span.keybinding')).textContent = this.options.keybinding;
}
this.updateClass();
this.updateLabel();
this.updateTooltip();
this.updateEnabled();
this.updateChecked();
}
focus(): void {
super.focus();
if (this.label) {
this.label.focus();
}
}
updateLabel(): void {
if (this.options.label && this.label) {
this.label.textContent = this.getAction().label;
}
}
updateTooltip(): void {
let title: string | null = null;
if (this.getAction().tooltip) {
title = this.getAction().tooltip;
} else if (!this.options.label && this.getAction().label && this.options.icon) {
title = this.getAction().label;
if (this.options.keybinding) {
title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding);
}
}
if (title && this.label) {
this.label.title = title;
}
}
updateClass(): void {
if (this.cssClass && this.label) {
this.label.classList.remove(...this.cssClass.split(' '));
}
if (this.options.icon) {
this.cssClass = this.getAction().class;
if (this.label) {
this.label.classList.add('codicon');
if (this.cssClass) {
this.label.classList.add(...this.cssClass.split(' '));
}
}
this.updateEnabled();
} else {
if (this.label) {
this.label.classList.remove('codicon');
}
}
}
updateEnabled(): void {
if (this.getAction().enabled) {
if (this.label) {
this.label.removeAttribute('aria-disabled');
this.label.classList.remove('disabled');
this.label.tabIndex = 0;
}
if (this.element) {
this.element.classList.remove('disabled');
}
} else {
if (this.label) {
this.label.setAttribute('aria-disabled', 'true');
this.label.classList.add('disabled');
removeTabIndexAndUpdateFocus(this.label);
}
if (this.element) {
this.element.classList.add('disabled');
}
}
}
updateChecked(): void {
if (this.label) {
if (this.getAction().checked) {
this.label.classList.add('checked');
} else {
this.label.classList.remove('checked');
}
}
}
}
export class SelectActionViewItem extends BaseActionViewItem {
protected selectBox: SelectBox;
constructor(ctx: unknown, action: IAction, options: ISelectOptionItem[], selected: number, contextViewProvider: IContextViewProvider, selectBoxOptions?: ISelectBoxOptions) {
super(ctx, action);
this.selectBox = new SelectBox(options, selected, contextViewProvider, undefined, selectBoxOptions);
this._register(this.selectBox);
this.registerListeners();
}
setOptions(options: ISelectOptionItem[], selected?: number): void {
this.selectBox.setOptions(options, selected);
}
select(index: number): void {
this.selectBox.select(index);
}
private registerListeners(): void {
this._register(this.selectBox.onDidSelect(e => {
this.actionRunner.run(this._action, this.getActionContext(e.selected, e.index));
}));
}
protected getActionContext(option: string, index: number) {
return option;
}
focus(): void {
if (this.selectBox) {
this.selectBox.focus();
}
}
blur(): void {
if (this.selectBox) {
this.selectBox.blur();
}
}
render(container: HTMLElement): void {
this.selectBox.render(container);
}
}

View File

@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-action-bar {
text-align: right;
white-space: nowrap;
}
.monaco-action-bar .actions-container {
display: flex;
margin: 0 auto;
padding: 0;
width: 100%;
justify-content: flex-end;
}
.monaco-action-bar.vertical .actions-container {
display: inline-block;
}
.monaco-action-bar.reverse .actions-container {
flex-direction: row-reverse;
}
.monaco-action-bar .action-item {
cursor: pointer;
display: inline-block;
transition: transform 50ms ease;
position: relative; /* DO NOT REMOVE - this is the key to preventing the ghosting icon bug in Chrome 42 */
}
.monaco-action-bar .action-item.disabled {
cursor: default;
}
.monaco-action-bar.animated .action-item.active {
transform: scale(1.272019649, 1.272019649); /* 1.272019649 = √φ */
}
.monaco-action-bar .action-item .icon,
.monaco-action-bar .action-item .codicon {
display: inline-block;
}
.monaco-action-bar .action-item .codicon {
display: flex;
align-items: center;
}
.monaco-action-bar .action-label {
font-size: 11px;
margin-right: 4px;
}
.monaco-action-bar .action-item.disabled .action-label,
.monaco-action-bar .action-item.disabled .action-label:hover {
opacity: 0.4;
}
/* Vertical actions */
.monaco-action-bar.vertical {
text-align: left;
}
.monaco-action-bar.vertical .action-item {
display: block;
}
.monaco-action-bar.vertical .action-label.separator {
display: block;
border-bottom: 1px solid #bbb;
padding-top: 1px;
margin-left: .8em;
margin-right: .8em;
}
.monaco-action-bar.animated.vertical .action-item.active {
transform: translate(5px, 0);
}
.secondary-actions .monaco-action-bar .action-label {
margin-left: 6px;
}
/* Action Items */
.monaco-action-bar .action-item.select-container {
overflow: hidden; /* somehow the dropdown overflows its container, we prevent it here to not push */
flex: 1;
max-width: 170px;
min-width: 60px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
}
.monaco-action-bar .action-item.action-dropdown-item {
display: flex;
}
.monaco-action-bar .action-item.action-dropdown-item > .action-label {
margin-right: 1px;
}
.monaco-action-bar .action-item.action-dropdown-item > .monaco-dropdown {
margin-right: 4px;
}

View File

@ -0,0 +1,536 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./actionbar';
import { Disposable, dispose } from 'vs/base/common/lifecycle';
import { IAction, IActionRunner, ActionRunner, IRunEvent, Separator, IActionViewItem, IActionViewItemProvider } from 'vs/base/common/actions';
import * as DOM from 'vs/base/browser/dom';
import * as types from 'vs/base/common/types';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Emitter } from 'vs/base/common/event';
import { IActionViewItemOptions, ActionViewItem, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
export const enum ActionsOrientation {
HORIZONTAL,
HORIZONTAL_REVERSE,
VERTICAL,
VERTICAL_REVERSE,
}
export interface ActionTrigger {
keys?: KeyCode[];
keyDown: boolean;
}
export interface IActionBarOptions {
readonly orientation?: ActionsOrientation;
readonly context?: any;
readonly actionViewItemProvider?: IActionViewItemProvider;
readonly actionRunner?: IActionRunner;
readonly ariaLabel?: string;
readonly animated?: boolean;
readonly triggerKeys?: ActionTrigger;
readonly allowContextMenu?: boolean;
readonly preventLoopNavigation?: boolean;
readonly ignoreOrientationForPreviousAndNextKey?: boolean;
}
export interface IActionOptions extends IActionViewItemOptions {
index?: number;
}
export class ActionBar extends Disposable implements IActionRunner {
private readonly options: IActionBarOptions;
private _actionRunner: IActionRunner;
private _context: unknown;
private readonly _orientation: ActionsOrientation;
private readonly _triggerKeys: {
keys: KeyCode[];
keyDown: boolean;
};
private _actionIds: string[];
// View Items
viewItems: IActionViewItem[];
protected focusedItem?: number;
private focusTracker: DOM.IFocusTracker;
// Elements
domNode: HTMLElement;
protected actionsList: HTMLElement;
private _onDidBlur = this._register(new Emitter<void>());
readonly onDidBlur = this._onDidBlur.event;
private _onDidCancel = this._register(new Emitter<void>({ onFirstListenerAdd: () => this.cancelHasListener = true }));
readonly onDidCancel = this._onDidCancel.event;
private cancelHasListener = false;
private _onDidRun = this._register(new Emitter<IRunEvent>());
readonly onDidRun = this._onDidRun.event;
private _onDidBeforeRun = this._register(new Emitter<IRunEvent>());
readonly onDidBeforeRun = this._onDidBeforeRun.event;
constructor(container: HTMLElement, options: IActionBarOptions = {}) {
super();
this.options = options;
this._context = options.context ?? null;
this._orientation = this.options.orientation ?? ActionsOrientation.HORIZONTAL;
this._triggerKeys = {
keyDown: this.options.triggerKeys?.keyDown ?? false,
keys: this.options.triggerKeys?.keys ?? [KeyCode.Enter, KeyCode.Space]
};
if (this.options.actionRunner) {
this._actionRunner = this.options.actionRunner;
} else {
this._actionRunner = new ActionRunner();
this._register(this._actionRunner);
}
this._register(this._actionRunner.onDidRun(e => this._onDidRun.fire(e)));
this._register(this._actionRunner.onDidBeforeRun(e => this._onDidBeforeRun.fire(e)));
this._actionIds = [];
this.viewItems = [];
this.focusedItem = undefined;
this.domNode = document.createElement('div');
this.domNode.className = 'monaco-action-bar';
if (options.animated !== false) {
this.domNode.classList.add('animated');
}
let previousKeys: KeyCode[];
let nextKeys: KeyCode[];
switch (this._orientation) {
case ActionsOrientation.HORIZONTAL:
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.LeftArrow];
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.RightArrow];
break;
case ActionsOrientation.HORIZONTAL_REVERSE:
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.RightArrow];
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.LeftArrow];
this.domNode.className += ' reverse';
break;
case ActionsOrientation.VERTICAL:
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.UpArrow];
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.DownArrow];
this.domNode.className += ' vertical';
break;
case ActionsOrientation.VERTICAL_REVERSE:
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.DownArrow];
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.UpArrow];
this.domNode.className += ' vertical reverse';
break;
}
this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e);
let eventHandled = true;
if (previousKeys && (event.equals(previousKeys[0]) || event.equals(previousKeys[1]))) {
eventHandled = this.focusPrevious();
} else if (nextKeys && (event.equals(nextKeys[0]) || event.equals(nextKeys[1]))) {
eventHandled = this.focusNext();
} else if (event.equals(KeyCode.Escape) && this.cancelHasListener) {
this._onDidCancel.fire();
} else if (this.isTriggerKeyEvent(event)) {
// Staying out of the else branch even if not triggered
if (this._triggerKeys.keyDown) {
this.doTrigger(event);
}
} else {
eventHandled = false;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
}
}));
this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.KEY_UP, e => {
const event = new StandardKeyboardEvent(e);
// Run action on Enter/Space
if (this.isTriggerKeyEvent(event)) {
if (!this._triggerKeys.keyDown) {
this.doTrigger(event);
}
event.preventDefault();
event.stopPropagation();
}
// Recompute focused item
else if (event.equals(KeyCode.Tab) || event.equals(KeyMod.Shift | KeyCode.Tab)) {
this.updateFocusedItem();
}
}));
this.focusTracker = this._register(DOM.trackFocus(this.domNode));
this._register(this.focusTracker.onDidBlur(() => {
if (DOM.getActiveElement() === this.domNode || !DOM.isAncestor(DOM.getActiveElement(), this.domNode)) {
this._onDidBlur.fire();
this.focusedItem = undefined;
}
}));
this._register(this.focusTracker.onDidFocus(() => this.updateFocusedItem()));
this.actionsList = document.createElement('ul');
this.actionsList.className = 'actions-container';
this.actionsList.setAttribute('role', 'toolbar');
if (this.options.ariaLabel) {
this.actionsList.setAttribute('aria-label', this.options.ariaLabel);
}
this.domNode.appendChild(this.actionsList);
container.appendChild(this.domNode);
}
setAriaLabel(label: string): void {
if (label) {
this.actionsList.setAttribute('aria-label', label);
} else {
this.actionsList.removeAttribute('aria-label');
}
}
private isTriggerKeyEvent(event: StandardKeyboardEvent): boolean {
let ret = false;
this._triggerKeys.keys.forEach(keyCode => {
ret = ret || event.equals(keyCode);
});
return ret;
}
private updateFocusedItem(): void {
for (let i = 0; i < this.actionsList.children.length; i++) {
const elem = this.actionsList.children[i];
if (DOM.isAncestor(DOM.getActiveElement(), elem)) {
this.focusedItem = i;
break;
}
}
}
get context(): any {
return this._context;
}
set context(context: any) {
this._context = context;
this.viewItems.forEach(i => i.setActionContext(context));
}
get actionRunner(): IActionRunner {
return this._actionRunner;
}
set actionRunner(actionRunner: IActionRunner) {
if (actionRunner) {
this._actionRunner = actionRunner;
this.viewItems.forEach(item => item.actionRunner = actionRunner);
}
}
getContainer(): HTMLElement {
return this.domNode;
}
hasAction(action: IAction): boolean {
return this._actionIds.includes(action.id);
}
push(arg: IAction | ReadonlyArray<IAction>, options: IActionOptions = {}): void {
const actions: ReadonlyArray<IAction> = Array.isArray(arg) ? arg : [arg];
let index = types.isNumber(options.index) ? options.index : null;
actions.forEach((action: IAction) => {
const actionViewItemElement = document.createElement('li');
actionViewItemElement.className = 'action-item';
actionViewItemElement.setAttribute('role', 'presentation');
// Prevent native context menu on actions
if (!this.options.allowContextMenu) {
this._register(DOM.addDisposableListener(actionViewItemElement, DOM.EventType.CONTEXT_MENU, (e: DOM.EventLike) => {
DOM.EventHelper.stop(e, true);
}));
}
let item: IActionViewItem | undefined;
if (this.options.actionViewItemProvider) {
item = this.options.actionViewItemProvider(action);
}
if (!item) {
item = new ActionViewItem(this.context, action, options);
}
item.actionRunner = this._actionRunner;
item.setActionContext(this.context);
item.render(actionViewItemElement);
if (index === null || index < 0 || index >= this.actionsList.children.length) {
this.actionsList.appendChild(actionViewItemElement);
this.viewItems.push(item);
this._actionIds.push(action.id);
} else {
this.actionsList.insertBefore(actionViewItemElement, this.actionsList.children[index]);
this.viewItems.splice(index, 0, item);
this._actionIds.splice(index, 0, action.id);
index++;
}
});
if (this.focusedItem) {
// After a clear actions might be re-added to simply toggle some actions. We should preserve focus #97128
this.focus(this.focusedItem);
}
}
getWidth(index: number): number {
if (index >= 0 && index < this.actionsList.children.length) {
const item = this.actionsList.children.item(index);
if (item) {
return item.clientWidth;
}
}
return 0;
}
getHeight(index: number): number {
if (index >= 0 && index < this.actionsList.children.length) {
const item = this.actionsList.children.item(index);
if (item) {
return item.clientHeight;
}
}
return 0;
}
pull(index: number): void {
if (index >= 0 && index < this.viewItems.length) {
this.actionsList.removeChild(this.actionsList.childNodes[index]);
dispose(this.viewItems.splice(index, 1));
this._actionIds.splice(index, 1);
}
}
clear(): void {
dispose(this.viewItems);
this.viewItems = [];
this._actionIds = [];
DOM.clearNode(this.actionsList);
}
length(): number {
return this.viewItems.length;
}
isEmpty(): boolean {
return this.viewItems.length === 0;
}
focus(index?: number): void;
focus(selectFirst?: boolean): void;
focus(arg?: number | boolean): void {
let selectFirst: boolean = false;
let index: number | undefined = undefined;
if (arg === undefined) {
selectFirst = true;
} else if (typeof arg === 'number') {
index = arg;
} else if (typeof arg === 'boolean') {
selectFirst = arg;
}
if (selectFirst && typeof this.focusedItem === 'undefined') {
const firstEnabled = this.viewItems.findIndex(item => item.isEnabled());
// Focus the first enabled item
this.focusedItem = firstEnabled === -1 ? undefined : firstEnabled;
this.updateFocus();
} else {
if (index !== undefined) {
this.focusedItem = index;
}
this.updateFocus();
}
}
protected focusNext(): boolean {
if (typeof this.focusedItem === 'undefined') {
this.focusedItem = this.viewItems.length - 1;
}
const startIndex = this.focusedItem;
let item: IActionViewItem;
do {
if (this.options.preventLoopNavigation && this.focusedItem + 1 >= this.viewItems.length) {
this.focusedItem = startIndex;
return false;
}
this.focusedItem = (this.focusedItem + 1) % this.viewItems.length;
item = this.viewItems[this.focusedItem];
} while (this.focusedItem !== startIndex && !item.isEnabled());
if (this.focusedItem === startIndex && !item.isEnabled()) {
this.focusedItem = undefined;
}
this.updateFocus();
return true;
}
protected focusPrevious(): boolean {
if (typeof this.focusedItem === 'undefined') {
this.focusedItem = 0;
}
const startIndex = this.focusedItem;
let item: IActionViewItem;
do {
this.focusedItem = this.focusedItem - 1;
if (this.focusedItem < 0) {
if (this.options.preventLoopNavigation) {
this.focusedItem = startIndex;
return false;
}
this.focusedItem = this.viewItems.length - 1;
}
item = this.viewItems[this.focusedItem];
} while (this.focusedItem !== startIndex && !item.isEnabled());
if (this.focusedItem === startIndex && !item.isEnabled()) {
this.focusedItem = undefined;
}
this.updateFocus(true);
return true;
}
protected updateFocus(fromRight?: boolean, preventScroll?: boolean): void {
if (typeof this.focusedItem === 'undefined') {
this.actionsList.focus({ preventScroll });
}
for (let i = 0; i < this.viewItems.length; i++) {
const item = this.viewItems[i];
const actionViewItem = item;
if (i === this.focusedItem) {
if (types.isFunction(actionViewItem.isEnabled)) {
if (actionViewItem.isEnabled() && types.isFunction(actionViewItem.focus)) {
actionViewItem.focus(fromRight);
} else {
this.actionsList.focus({ preventScroll });
}
}
} else {
if (types.isFunction(actionViewItem.blur)) {
actionViewItem.blur();
}
}
}
}
private doTrigger(event: StandardKeyboardEvent): void {
if (typeof this.focusedItem === 'undefined') {
return; //nothing to focus
}
// trigger action
const actionViewItem = this.viewItems[this.focusedItem];
if (actionViewItem instanceof BaseActionViewItem) {
const context = (actionViewItem._context === null || actionViewItem._context === undefined) ? event : actionViewItem._context;
this.run(actionViewItem._action, context);
}
}
run(action: IAction, context?: unknown): Promise<void> {
return this._actionRunner.run(action, context);
}
dispose(): void {
dispose(this.viewItems);
this.viewItems = [];
this._actionIds = [];
this.getContainer().remove();
super.dispose();
}
}
export function prepareActions(actions: IAction[]): IAction[] {
if (!actions.length) {
return actions;
}
// Clean up leading separators
let firstIndexOfAction = -1;
for (let i = 0; i < actions.length; i++) {
if (actions[i].id === Separator.ID) {
continue;
}
firstIndexOfAction = i;
break;
}
if (firstIndexOfAction === -1) {
return [];
}
actions = actions.slice(firstIndexOfAction);
// Clean up trailing separators
for (let h = actions.length - 1; h >= 0; h--) {
const isSeparator = actions[h].id === Separator.ID;
if (isSeparator) {
actions.splice(h, 1);
} else {
break;
}
}
// Clean up separator duplicates
let foundAction = false;
for (let k = actions.length - 1; k >= 0; k--) {
const isSeparator = actions[k].id === Separator.ID;
if (isSeparator && !foundAction) {
actions.splice(k, 1);
} else if (!isSeparator) {
foundAction = true;
} else if (isSeparator) {
foundAction = false;
}
}
return actions;
}

View File

@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-aria-container {
position: absolute; /* try to hide from window but not from screen readers */
left:-999em;
}

View File

@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./aria';
import { isMacintosh } from 'vs/base/common/platform';
import * as dom from 'vs/base/browser/dom';
// Use a max length since we are inserting the whole msg in the DOM and that can cause browsers to freeze for long messages #94233
const MAX_MESSAGE_LENGTH = 20000;
let ariaContainer: HTMLElement;
let alertContainer: HTMLElement;
let alertContainer2: HTMLElement;
let statusContainer: HTMLElement;
let statusContainer2: HTMLElement;
export function setARIAContainer(parent: HTMLElement) {
ariaContainer = document.createElement('div');
ariaContainer.className = 'monaco-aria-container';
const createAlertContainer = () => {
const element = document.createElement('div');
element.className = 'monaco-alert';
element.setAttribute('role', 'alert');
element.setAttribute('aria-atomic', 'true');
ariaContainer.appendChild(element);
return element;
};
alertContainer = createAlertContainer();
alertContainer2 = createAlertContainer();
const createStatusContainer = () => {
const element = document.createElement('div');
element.className = 'monaco-status';
element.setAttribute('role', 'complementary');
element.setAttribute('aria-live', 'polite');
element.setAttribute('aria-atomic', 'true');
ariaContainer.appendChild(element);
return element;
};
statusContainer = createStatusContainer();
statusContainer2 = createStatusContainer();
parent.appendChild(ariaContainer);
}
/**
* Given the provided message, will make sure that it is read as alert to screen readers.
*/
export function alert(msg: string): void {
if (!ariaContainer) {
return;
}
// Use alternate containers such that duplicated messages get read out by screen readers #99466
if (alertContainer.textContent !== msg) {
dom.clearNode(alertContainer2);
insertMessage(alertContainer, msg);
} else {
dom.clearNode(alertContainer);
insertMessage(alertContainer2, msg);
}
}
/**
* Given the provided message, will make sure that it is read as status to screen readers.
*/
export function status(msg: string): void {
if (!ariaContainer) {
return;
}
if (isMacintosh) {
alert(msg); // VoiceOver does not seem to support status role
} else {
if (statusContainer.textContent !== msg) {
dom.clearNode(statusContainer2);
insertMessage(statusContainer, msg);
} else {
dom.clearNode(statusContainer);
insertMessage(statusContainer2, msg);
}
}
}
function insertMessage(target: HTMLElement, msg: string): void {
dom.clearNode(target);
if (msg.length > MAX_MESSAGE_LENGTH) {
msg = msg.substr(0, MAX_MESSAGE_LENGTH);
}
target.textContent = msg;
// See https://www.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/
target.style.visibility = 'hidden';
target.style.visibility = 'visible';
}

View File

@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-breadcrumbs {
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
outline-style: none;
}
.monaco-breadcrumbs .monaco-breadcrumb-item {
display: flex;
align-items: center;
flex: 0 1 auto;
white-space: nowrap;
cursor: pointer;
align-self: center;
height: 100%;
outline: none;
}
.monaco-breadcrumbs .monaco-breadcrumb-item .codicon-breadcrumb-separator {
color: inherit;
}
.monaco-breadcrumbs .monaco-breadcrumb-item:first-of-type::before {
content: ' ';
}

View File

@ -0,0 +1,354 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { commonPrefixLength } from 'vs/base/common/arrays';
import { Color } from 'vs/base/common/color';
import { Emitter, Event } from 'vs/base/common/event';
import { dispose, IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { Codicon, registerIcon } from 'vs/base/common/codicons';
import 'vs/css!./breadcrumbsWidget';
export abstract class BreadcrumbsItem {
dispose(): void { }
abstract equals(other: BreadcrumbsItem): boolean;
abstract render(container: HTMLElement): void;
}
export class SimpleBreadcrumbsItem extends BreadcrumbsItem {
constructor(
readonly text: string,
readonly title: string = text
) {
super();
}
equals(other: this) {
return other === this || other instanceof SimpleBreadcrumbsItem && other.text === this.text && other.title === this.title;
}
render(container: HTMLElement): void {
let node = document.createElement('div');
node.title = this.title;
node.innerText = this.text;
container.appendChild(node);
}
}
export interface IBreadcrumbsWidgetStyles {
breadcrumbsBackground?: Color;
breadcrumbsForeground?: Color;
breadcrumbsHoverForeground?: Color;
breadcrumbsFocusForeground?: Color;
breadcrumbsFocusAndSelectionForeground?: Color;
}
export interface IBreadcrumbsItemEvent {
type: 'select' | 'focus';
item: BreadcrumbsItem;
node: HTMLElement;
payload: any;
}
const breadcrumbSeparatorIcon = registerIcon('breadcrumb-separator', Codicon.chevronRight);
export class BreadcrumbsWidget {
private readonly _disposables = new DisposableStore();
private readonly _domNode: HTMLDivElement;
private readonly _styleElement: HTMLStyleElement;
private readonly _scrollable: DomScrollableElement;
private readonly _onDidSelectItem = new Emitter<IBreadcrumbsItemEvent>();
private readonly _onDidFocusItem = new Emitter<IBreadcrumbsItemEvent>();
private readonly _onDidChangeFocus = new Emitter<boolean>();
readonly onDidSelectItem: Event<IBreadcrumbsItemEvent> = this._onDidSelectItem.event;
readonly onDidFocusItem: Event<IBreadcrumbsItemEvent> = this._onDidFocusItem.event;
readonly onDidChangeFocus: Event<boolean> = this._onDidChangeFocus.event;
private readonly _items = new Array<BreadcrumbsItem>();
private readonly _nodes = new Array<HTMLDivElement>();
private readonly _freeNodes = new Array<HTMLDivElement>();
private _focusedItemIdx: number = -1;
private _selectedItemIdx: number = -1;
private _pendingLayout: IDisposable | undefined;
private _dimension: dom.Dimension | undefined;
constructor(
container: HTMLElement,
horizontalScrollbarSize: number,
) {
this._domNode = document.createElement('div');
this._domNode.className = 'monaco-breadcrumbs';
this._domNode.tabIndex = 0;
this._domNode.setAttribute('role', 'list');
this._scrollable = new DomScrollableElement(this._domNode, {
vertical: ScrollbarVisibility.Hidden,
horizontal: ScrollbarVisibility.Auto,
horizontalScrollbarSize,
useShadows: false,
scrollYToX: true
});
this._disposables.add(this._scrollable);
this._disposables.add(dom.addStandardDisposableListener(this._domNode, 'click', e => this._onClick(e)));
container.appendChild(this._scrollable.getDomNode());
this._styleElement = dom.createStyleSheet(this._domNode);
const focusTracker = dom.trackFocus(this._domNode);
this._disposables.add(focusTracker);
this._disposables.add(focusTracker.onDidBlur(_ => this._onDidChangeFocus.fire(false)));
this._disposables.add(focusTracker.onDidFocus(_ => this._onDidChangeFocus.fire(true)));
}
setHorizontalScrollbarSize(size: number) {
this._scrollable.updateOptions({
horizontalScrollbarSize: size
});
}
dispose(): void {
this._disposables.dispose();
this._pendingLayout?.dispose();
this._onDidSelectItem.dispose();
this._onDidFocusItem.dispose();
this._onDidChangeFocus.dispose();
this._domNode.remove();
this._nodes.length = 0;
this._freeNodes.length = 0;
}
layout(dim: dom.Dimension | undefined): void {
if (dim && dom.Dimension.equals(dim, this._dimension)) {
return;
}
this._pendingLayout?.dispose();
if (dim) {
// only measure
this._pendingLayout = this._updateDimensions(dim);
} else {
this._pendingLayout = this._updateScrollbar();
}
}
private _updateDimensions(dim: dom.Dimension): IDisposable {
const disposables = new DisposableStore();
disposables.add(dom.modify(() => {
this._dimension = dim;
this._domNode.style.width = `${dim.width}px`;
this._domNode.style.height = `${dim.height}px`;
disposables.add(this._updateScrollbar());
}));
return disposables;
}
private _updateScrollbar(): IDisposable {
return dom.measure(() => {
dom.measure(() => { // double RAF
this._scrollable.setRevealOnScroll(false);
this._scrollable.scanDomNode();
this._scrollable.setRevealOnScroll(true);
});
});
}
style(style: IBreadcrumbsWidgetStyles): void {
let content = '';
if (style.breadcrumbsBackground) {
content += `.monaco-breadcrumbs { background-color: ${style.breadcrumbsBackground}}`;
}
if (style.breadcrumbsForeground) {
content += `.monaco-breadcrumbs .monaco-breadcrumb-item { color: ${style.breadcrumbsForeground}}\n`;
}
if (style.breadcrumbsFocusForeground) {
content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused { color: ${style.breadcrumbsFocusForeground}}\n`;
}
if (style.breadcrumbsFocusAndSelectionForeground) {
content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused.selected { color: ${style.breadcrumbsFocusAndSelectionForeground}}\n`;
}
if (style.breadcrumbsHoverForeground) {
content += `.monaco-breadcrumbs .monaco-breadcrumb-item:hover:not(.focused):not(.selected) { color: ${style.breadcrumbsHoverForeground}}\n`;
}
if (this._styleElement.innerText !== content) {
this._styleElement.innerText = content;
}
}
domFocus(): void {
let idx = this._focusedItemIdx >= 0 ? this._focusedItemIdx : this._items.length - 1;
if (idx >= 0 && idx < this._items.length) {
this._focus(idx, undefined);
} else {
this._domNode.focus();
}
}
isDOMFocused(): boolean {
let candidate = document.activeElement;
while (candidate) {
if (this._domNode === candidate) {
return true;
}
candidate = candidate.parentElement;
}
return false;
}
getFocused(): BreadcrumbsItem {
return this._items[this._focusedItemIdx];
}
setFocused(item: BreadcrumbsItem | undefined, payload?: any): void {
this._focus(this._items.indexOf(item!), payload);
}
focusPrev(payload?: any): any {
if (this._focusedItemIdx > 0) {
this._focus(this._focusedItemIdx - 1, payload);
}
}
focusNext(payload?: any): any {
if (this._focusedItemIdx + 1 < this._nodes.length) {
this._focus(this._focusedItemIdx + 1, payload);
}
}
private _focus(nth: number, payload: any): void {
this._focusedItemIdx = -1;
for (let i = 0; i < this._nodes.length; i++) {
const node = this._nodes[i];
if (i !== nth) {
node.classList.remove('focused');
} else {
this._focusedItemIdx = i;
node.classList.add('focused');
node.focus();
}
}
this._reveal(this._focusedItemIdx, true);
this._onDidFocusItem.fire({ type: 'focus', item: this._items[this._focusedItemIdx], node: this._nodes[this._focusedItemIdx], payload });
}
reveal(item: BreadcrumbsItem): void {
let idx = this._items.indexOf(item);
if (idx >= 0) {
this._reveal(idx, false);
}
}
private _reveal(nth: number, minimal: boolean): void {
const node = this._nodes[nth];
if (node) {
const { width } = this._scrollable.getScrollDimensions();
const { scrollLeft } = this._scrollable.getScrollPosition();
if (!minimal || node.offsetLeft > scrollLeft + width || node.offsetLeft < scrollLeft) {
this._scrollable.setRevealOnScroll(false);
this._scrollable.setScrollPosition({ scrollLeft: node.offsetLeft });
this._scrollable.setRevealOnScroll(true);
}
}
}
getSelection(): BreadcrumbsItem {
return this._items[this._selectedItemIdx];
}
setSelection(item: BreadcrumbsItem | undefined, payload?: any): void {
this._select(this._items.indexOf(item!), payload);
}
private _select(nth: number, payload: any): void {
this._selectedItemIdx = -1;
for (let i = 0; i < this._nodes.length; i++) {
const node = this._nodes[i];
if (i !== nth) {
node.classList.remove('selected');
} else {
this._selectedItemIdx = i;
node.classList.add('selected');
}
}
this._onDidSelectItem.fire({ type: 'select', item: this._items[this._selectedItemIdx], node: this._nodes[this._selectedItemIdx], payload });
}
getItems(): readonly BreadcrumbsItem[] {
return this._items;
}
setItems(items: BreadcrumbsItem[]): void {
let prefix: number | undefined;
let removed: BreadcrumbsItem[] = [];
try {
prefix = commonPrefixLength(this._items, items, (a, b) => a.equals(b));
removed = this._items.splice(prefix, this._items.length - prefix, ...items.slice(prefix));
this._render(prefix);
dispose(removed);
this._focus(-1, undefined);
} catch (e) {
let newError = new Error(`BreadcrumbsItem#setItems: newItems: ${items.length}, prefix: ${prefix}, removed: ${removed.length}`);
newError.name = e.name;
newError.stack = e.stack;
throw newError;
}
}
private _render(start: number): void {
for (; start < this._items.length && start < this._nodes.length; start++) {
let item = this._items[start];
let node = this._nodes[start];
this._renderItem(item, node);
}
// case a: more nodes -> remove them
while (start < this._nodes.length) {
const free = this._nodes.pop();
if (free) {
this._freeNodes.push(free);
free.remove();
}
}
// case b: more items -> render them
for (; start < this._items.length; start++) {
let item = this._items[start];
let node = this._freeNodes.length > 0 ? this._freeNodes.pop() : document.createElement('div');
if (node) {
this._renderItem(item, node);
this._domNode.appendChild(node);
this._nodes.push(node);
}
}
this.layout(undefined);
}
private _renderItem(item: BreadcrumbsItem, container: HTMLDivElement): void {
dom.clearNode(container);
container.className = '';
item.render(container);
container.tabIndex = -1;
container.setAttribute('role', 'listitem');
container.classList.add('monaco-breadcrumb-item');
const iconContainer = dom.$(breadcrumbSeparatorIcon.cssSelector);
container.appendChild(iconContainer);
}
private _onClick(event: IMouseEvent): void {
for (let el: HTMLElement | null = event.target; el; el = el.parentElement) {
let idx = this._nodes.indexOf(el as HTMLDivElement);
if (idx >= 0) {
this._focus(idx, event);
this._select(idx, event);
break;
}
}
}
}

View File

@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-text-button {
box-sizing: border-box;
display: flex;
width: 100%;
padding: 4px;
text-align: center;
cursor: pointer;
outline-offset: 2px !important;
justify-content: center;
align-items: center;
}
.monaco-text-button:hover {
text-decoration: none !important;
}
.monaco-button.disabled {
opacity: 0.4;
cursor: default;
}
.monaco-button > .codicon {
margin: 0 0.2em;
color: inherit !important;
}

View File

@ -0,0 +1,263 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./button';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
import { Event as BaseEvent, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch';
import { renderCodicons } from 'vs/base/browser/codicons';
import { addDisposableListener, IFocusTracker, EventType, EventHelper, trackFocus, reset, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom';
export interface IButtonOptions extends IButtonStyles {
readonly title?: boolean | string;
readonly supportCodicons?: boolean;
readonly secondary?: boolean;
}
export interface IButtonStyles {
buttonBackground?: Color;
buttonHoverBackground?: Color;
buttonForeground?: Color;
buttonSecondaryBackground?: Color;
buttonSecondaryHoverBackground?: Color;
buttonSecondaryForeground?: Color;
buttonBorder?: Color;
}
const defaultOptions: IButtonStyles = {
buttonBackground: Color.fromHex('#0E639C'),
buttonHoverBackground: Color.fromHex('#006BB3'),
buttonForeground: Color.white
};
export class Button extends Disposable {
private _element: HTMLElement;
private options: IButtonOptions;
private buttonBackground: Color | undefined;
private buttonHoverBackground: Color | undefined;
private buttonForeground: Color | undefined;
private buttonSecondaryBackground: Color | undefined;
private buttonSecondaryHoverBackground: Color | undefined;
private buttonSecondaryForeground: Color | undefined;
private buttonBorder: Color | undefined;
private _onDidClick = this._register(new Emitter<Event>());
get onDidClick(): BaseEvent<Event> { return this._onDidClick.event; }
private focusTracker: IFocusTracker;
constructor(container: HTMLElement, options?: IButtonOptions) {
super();
this.options = options || Object.create(null);
mixin(this.options, defaultOptions, false);
this.buttonForeground = this.options.buttonForeground;
this.buttonBackground = this.options.buttonBackground;
this.buttonHoverBackground = this.options.buttonHoverBackground;
this.buttonSecondaryForeground = this.options.buttonSecondaryForeground;
this.buttonSecondaryBackground = this.options.buttonSecondaryBackground;
this.buttonSecondaryHoverBackground = this.options.buttonSecondaryHoverBackground;
this.buttonBorder = this.options.buttonBorder;
this._element = document.createElement('a');
this._element.classList.add('monaco-button');
this._element.tabIndex = 0;
this._element.setAttribute('role', 'button');
container.appendChild(this._element);
this._register(Gesture.addTarget(this._element));
[EventType.CLICK, TouchEventType.Tap].forEach(eventType => {
this._register(addDisposableListener(this._element, eventType, e => {
if (!this.enabled) {
EventHelper.stop(e);
return;
}
this._onDidClick.fire(e);
}));
});
this._register(addDisposableListener(this._element, EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e);
let eventHandled = false;
if (this.enabled && (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space))) {
this._onDidClick.fire(e);
eventHandled = true;
} else if (event.equals(KeyCode.Escape)) {
this._element.blur();
eventHandled = true;
}
if (eventHandled) {
EventHelper.stop(event, true);
}
}));
this._register(addDisposableListener(this._element, EventType.MOUSE_OVER, e => {
if (!this._element.classList.contains('disabled')) {
this.setHoverBackground();
}
}));
this._register(addDisposableListener(this._element, EventType.MOUSE_OUT, e => {
this.applyStyles(); // restore standard styles
}));
// Also set hover background when button is focused for feedback
this.focusTracker = this._register(trackFocus(this._element));
this._register(this.focusTracker.onDidFocus(() => this.setHoverBackground()));
this._register(this.focusTracker.onDidBlur(() => this.applyStyles())); // restore standard styles
this.applyStyles();
}
private setHoverBackground(): void {
let hoverBackground;
if (this.options.secondary) {
hoverBackground = this.buttonSecondaryHoverBackground ? this.buttonSecondaryHoverBackground.toString() : null;
} else {
hoverBackground = this.buttonHoverBackground ? this.buttonHoverBackground.toString() : null;
}
if (hoverBackground) {
this._element.style.backgroundColor = hoverBackground;
}
}
style(styles: IButtonStyles): void {
this.buttonForeground = styles.buttonForeground;
this.buttonBackground = styles.buttonBackground;
this.buttonHoverBackground = styles.buttonHoverBackground;
this.buttonSecondaryForeground = styles.buttonSecondaryForeground;
this.buttonSecondaryBackground = styles.buttonSecondaryBackground;
this.buttonSecondaryHoverBackground = styles.buttonSecondaryHoverBackground;
this.buttonBorder = styles.buttonBorder;
this.applyStyles();
}
private applyStyles(): void {
if (this._element) {
let background, foreground;
if (this.options.secondary) {
foreground = this.buttonSecondaryForeground ? this.buttonSecondaryForeground.toString() : '';
background = this.buttonSecondaryBackground ? this.buttonSecondaryBackground.toString() : '';
} else {
foreground = this.buttonForeground ? this.buttonForeground.toString() : '';
background = this.buttonBackground ? this.buttonBackground.toString() : '';
}
const border = this.buttonBorder ? this.buttonBorder.toString() : '';
this._element.style.color = foreground;
this._element.style.backgroundColor = background;
this._element.style.borderWidth = border ? '1px' : '';
this._element.style.borderStyle = border ? 'solid' : '';
this._element.style.borderColor = border;
}
}
get element(): HTMLElement {
return this._element;
}
set label(value: string) {
this._element.classList.add('monaco-text-button');
if (this.options.supportCodicons) {
reset(this._element, ...renderCodicons(value));
} else {
this._element.textContent = value;
}
if (typeof this.options.title === 'string') {
this._element.title = this.options.title;
} else if (this.options.title) {
this._element.title = value;
}
}
set icon(iconClassName: string) {
this._element.classList.add(iconClassName);
}
set enabled(value: boolean) {
if (value) {
this._element.classList.remove('disabled');
this._element.setAttribute('aria-disabled', String(false));
this._element.tabIndex = 0;
} else {
this._element.classList.add('disabled');
this._element.setAttribute('aria-disabled', String(true));
removeTabIndexAndUpdateFocus(this._element);
}
}
get enabled() {
return !this._element.classList.contains('disabled');
}
focus(): void {
this._element.focus();
}
hasFocus(): boolean {
return this._element === document.activeElement;
}
}
export class ButtonGroup extends Disposable {
private _buttons: Button[] = [];
constructor(container: HTMLElement, count: number, options?: IButtonOptions) {
super();
this.create(container, count, options);
}
get buttons(): Button[] {
return this._buttons;
}
private create(container: HTMLElement, count: number, options?: IButtonOptions): void {
for (let index = 0; index < count; index++) {
const button = this._register(new Button(container, options));
this._buttons.push(button);
// Implement keyboard access in buttons if there are multiple
if (count > 1) {
this._register(addDisposableListener(button.element, EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e);
let eventHandled = true;
// Next / Previous Button
let buttonIndexToFocus: number | undefined;
if (event.equals(KeyCode.LeftArrow)) {
buttonIndexToFocus = index > 0 ? index - 1 : this._buttons.length - 1;
} else if (event.equals(KeyCode.RightArrow)) {
buttonIndexToFocus = index === this._buttons.length - 1 ? 0 : index + 1;
} else {
eventHandled = false;
}
if (eventHandled && typeof buttonIndexToFocus === 'number') {
this._buttons[buttonIndexToFocus].focus();
EventHelper.stop(e, true);
}
}));
}
}
}
}

View File

@ -0,0 +1,183 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SplitView, Orientation, ISplitViewStyles, IView as ISplitViewView } from 'vs/base/browser/ui/splitview/splitview';
import { $ } from 'vs/base/browser/dom';
import { Event } from 'vs/base/common/event';
import { IView, IViewSize } from 'vs/base/browser/ui/grid/grid';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Color } from 'vs/base/common/color';
import { IBoundarySashes } from 'vs/base/browser/ui/grid/gridview';
export interface CenteredViewState {
leftMarginRatio: number;
rightMarginRatio: number;
}
const GOLDEN_RATIO = {
leftMarginRatio: 0.1909,
rightMarginRatio: 0.1909
};
function createEmptyView(background: Color | undefined): ISplitViewView {
const element = $('.centered-layout-margin');
element.style.height = '100%';
if (background) {
element.style.backgroundColor = background.toString();
}
return {
element,
layout: () => undefined,
minimumSize: 60,
maximumSize: Number.POSITIVE_INFINITY,
onDidChange: Event.None
};
}
function toSplitViewView(view: IView, getHeight: () => number): ISplitViewView {
return {
element: view.element,
get maximumSize() { return view.maximumWidth; },
get minimumSize() { return view.minimumWidth; },
onDidChange: Event.map(view.onDidChange, e => e && e.width),
layout: (size, offset) => view.layout(size, getHeight(), 0, offset)
};
}
export interface ICenteredViewStyles extends ISplitViewStyles {
background: Color;
}
export class CenteredViewLayout implements IDisposable {
private splitView?: SplitView;
private width: number = 0;
private height: number = 0;
private style!: ICenteredViewStyles;
private didLayout = false;
private emptyViews: ISplitViewView[] | undefined;
private readonly splitViewDisposables = new DisposableStore();
constructor(private container: HTMLElement, private view: IView, public readonly state: CenteredViewState = { leftMarginRatio: GOLDEN_RATIO.leftMarginRatio, rightMarginRatio: GOLDEN_RATIO.rightMarginRatio }) {
this.container.appendChild(this.view.element);
// Make sure to hide the split view overflow like sashes #52892
this.container.style.overflow = 'hidden';
}
get minimumWidth(): number { return this.splitView ? this.splitView.minimumSize : this.view.minimumWidth; }
get maximumWidth(): number { return this.splitView ? this.splitView.maximumSize : this.view.maximumWidth; }
get minimumHeight(): number { return this.view.minimumHeight; }
get maximumHeight(): number { return this.view.maximumHeight; }
get onDidChange(): Event<IViewSize | undefined> { return this.view.onDidChange; }
private _boundarySashes: IBoundarySashes = {};
get boundarySashes(): IBoundarySashes { return this._boundarySashes; }
set boundarySashes(boundarySashes: IBoundarySashes) {
this._boundarySashes = boundarySashes;
if (!this.splitView) {
return;
}
this.splitView.orthogonalStartSash = boundarySashes.top;
this.splitView.orthogonalEndSash = boundarySashes.bottom;
}
layout(width: number, height: number): void {
this.width = width;
this.height = height;
if (this.splitView) {
this.splitView.layout(width);
if (!this.didLayout) {
this.resizeMargins();
}
} else {
this.view.layout(width, height, 0, 0);
}
this.didLayout = true;
}
private resizeMargins(): void {
if (!this.splitView) {
return;
}
this.splitView.resizeView(0, this.state.leftMarginRatio * this.width);
this.splitView.resizeView(2, this.state.rightMarginRatio * this.width);
}
isActive(): boolean {
return !!this.splitView;
}
styles(style: ICenteredViewStyles): void {
this.style = style;
if (this.splitView && this.emptyViews) {
this.splitView.style(this.style);
this.emptyViews[0].element.style.backgroundColor = this.style.background.toString();
this.emptyViews[1].element.style.backgroundColor = this.style.background.toString();
}
}
activate(active: boolean): void {
if (active === this.isActive()) {
return;
}
if (active) {
this.container.removeChild(this.view.element);
this.splitView = new SplitView(this.container, {
inverseAltBehavior: true,
orientation: Orientation.HORIZONTAL,
styles: this.style
});
this.splitView.orthogonalStartSash = this.boundarySashes.top;
this.splitView.orthogonalEndSash = this.boundarySashes.bottom;
this.splitViewDisposables.add(this.splitView.onDidSashChange(() => {
if (this.splitView) {
this.state.leftMarginRatio = this.splitView.getViewSize(0) / this.width;
this.state.rightMarginRatio = this.splitView.getViewSize(2) / this.width;
}
}));
this.splitViewDisposables.add(this.splitView.onDidSashReset(() => {
this.state.leftMarginRatio = GOLDEN_RATIO.leftMarginRatio;
this.state.rightMarginRatio = GOLDEN_RATIO.rightMarginRatio;
this.resizeMargins();
}));
this.splitView.layout(this.width);
this.splitView.addView(toSplitViewView(this.view, () => this.height), 0);
const backgroundColor = this.style ? this.style.background : undefined;
this.emptyViews = [createEmptyView(backgroundColor), createEmptyView(backgroundColor)];
this.splitView.addView(this.emptyViews[0], this.state.leftMarginRatio * this.width, 0);
this.splitView.addView(this.emptyViews[1], this.state.rightMarginRatio * this.width, 2);
} else {
if (this.splitView) {
this.container.removeChild(this.splitView.el);
}
this.splitViewDisposables.clear();
if (this.splitView) {
this.splitView.dispose();
}
this.splitView = undefined;
this.emptyViews = undefined;
this.container.appendChild(this.view.element);
}
}
isDefault(state: CenteredViewState): boolean {
return state.leftMarginRatio === GOLDEN_RATIO.leftMarginRatio && state.rightMarginRatio === GOLDEN_RATIO.rightMarginRatio;
}
dispose(): void {
this.splitViewDisposables.dispose();
if (this.splitView) {
this.splitView.dispose();
this.splitView = undefined;
}
}
}

View File

@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-custom-checkbox {
margin-left: 2px;
float: left;
cursor: pointer;
overflow: hidden;
opacity: 0.7;
width: 20px;
height: 20px;
border: 1px solid transparent;
padding: 1px;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.monaco-custom-checkbox:hover,
.monaco-custom-checkbox.checked {
opacity: 1;
}
.hc-black .monaco-custom-checkbox {
background: none;
}
.hc-black .monaco-custom-checkbox:hover {
background: none;
}
.monaco-custom-checkbox.monaco-simple-checkbox {
height: 18px;
width: 18px;
border: 1px solid transparent;
border-radius: 3px;
margin-right: 9px;
margin-left: 0px;
padding: 0px;
opacity: 1;
background-size: 16px !important;
}
/* hide check when unchecked */
.monaco-custom-checkbox.monaco-simple-checkbox:not(.checked)::before {
visibility: hidden;
}

View File

@ -0,0 +1,251 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./checkbox';
import * as DOM from 'vs/base/browser/dom';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Widget } from 'vs/base/browser/ui/widget';
import { Color } from 'vs/base/common/color';
import { Emitter, Event } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { Codicon } from 'vs/base/common/codicons';
import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
export interface ICheckboxOpts extends ICheckboxStyles {
readonly actionClassName?: string;
readonly icon?: Codicon;
readonly title: string;
readonly isChecked: boolean;
}
export interface ICheckboxStyles {
inputActiveOptionBorder?: Color;
inputActiveOptionForeground?: Color;
inputActiveOptionBackground?: Color;
}
export interface ISimpleCheckboxStyles {
checkboxBackground?: Color;
checkboxBorder?: Color;
checkboxForeground?: Color;
}
const defaultOpts = {
inputActiveOptionBorder: Color.fromHex('#007ACC00'),
inputActiveOptionForeground: Color.fromHex('#FFFFFF'),
inputActiveOptionBackground: Color.fromHex('#0E639C50')
};
export class CheckboxActionViewItem extends BaseActionViewItem {
protected checkbox: Checkbox | undefined;
protected readonly disposables = new DisposableStore();
render(container: HTMLElement): void {
this.element = container;
this.disposables.clear();
this.checkbox = new Checkbox({
actionClassName: this._action.class,
isChecked: this._action.checked,
title: this._action.label
});
this.disposables.add(this.checkbox);
this.disposables.add(this.checkbox.onChange(() => this._action.checked = !!this.checkbox && this.checkbox.checked, this));
this.element.appendChild(this.checkbox.domNode);
}
updateEnabled(): void {
if (this.checkbox) {
if (this.isEnabled()) {
this.checkbox.enable();
} else {
this.checkbox.disable();
}
}
}
updateChecked(): void {
if (this.checkbox) {
this.checkbox.checked = this._action.checked;
}
}
dispose(): void {
this.disposables.dispose();
super.dispose();
}
}
export class Checkbox extends Widget {
private readonly _onChange = this._register(new Emitter<boolean>());
readonly onChange: Event<boolean /* via keyboard */> = this._onChange.event;
private readonly _onKeyDown = this._register(new Emitter<IKeyboardEvent>());
readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;
private readonly _opts: ICheckboxOpts;
readonly domNode: HTMLElement;
private _checked: boolean;
constructor(opts: ICheckboxOpts) {
super();
this._opts = { ...defaultOpts, ...opts };
this._checked = this._opts.isChecked;
const classes = ['monaco-custom-checkbox'];
if (this._opts.icon) {
classes.push(this._opts.icon.classNames);
} else {
classes.push('codicon'); // todo@aeschli: remove once codicon fully adopted
}
if (this._opts.actionClassName) {
classes.push(this._opts.actionClassName);
}
if (this._checked) {
classes.push('checked');
}
this.domNode = document.createElement('div');
this.domNode.title = this._opts.title;
this.domNode.className = classes.join(' ');
this.domNode.tabIndex = 0;
this.domNode.setAttribute('role', 'checkbox');
this.domNode.setAttribute('aria-checked', String(this._checked));
this.domNode.setAttribute('aria-label', this._opts.title);
this.applyStyles();
this.onclick(this.domNode, (ev) => {
this.checked = !this._checked;
this._onChange.fire(false);
ev.preventDefault();
});
this.ignoreGesture(this.domNode);
this.onkeydown(this.domNode, (keyboardEvent) => {
if (keyboardEvent.keyCode === KeyCode.Space || keyboardEvent.keyCode === KeyCode.Enter) {
this.checked = !this._checked;
this._onChange.fire(true);
keyboardEvent.preventDefault();
return;
}
this._onKeyDown.fire(keyboardEvent);
});
}
get enabled(): boolean {
return this.domNode.getAttribute('aria-disabled') !== 'true';
}
focus(): void {
this.domNode.focus();
}
get checked(): boolean {
return this._checked;
}
set checked(newIsChecked: boolean) {
this._checked = newIsChecked;
this.domNode.setAttribute('aria-checked', String(this._checked));
this.domNode.classList.toggle('checked', this._checked);
this.applyStyles();
}
width(): number {
return 2 /*marginleft*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */;
}
style(styles: ICheckboxStyles): void {
if (styles.inputActiveOptionBorder) {
this._opts.inputActiveOptionBorder = styles.inputActiveOptionBorder;
}
if (styles.inputActiveOptionForeground) {
this._opts.inputActiveOptionForeground = styles.inputActiveOptionForeground;
}
if (styles.inputActiveOptionBackground) {
this._opts.inputActiveOptionBackground = styles.inputActiveOptionBackground;
}
this.applyStyles();
}
protected applyStyles(): void {
if (this.domNode) {
this.domNode.style.borderColor = this._checked && this._opts.inputActiveOptionBorder ? this._opts.inputActiveOptionBorder.toString() : 'transparent';
this.domNode.style.color = this._checked && this._opts.inputActiveOptionForeground ? this._opts.inputActiveOptionForeground.toString() : 'inherit';
this.domNode.style.backgroundColor = this._checked && this._opts.inputActiveOptionBackground ? this._opts.inputActiveOptionBackground.toString() : 'transparent';
}
}
enable(): void {
this.domNode.tabIndex = 0;
this.domNode.setAttribute('aria-disabled', String(false));
}
disable(): void {
DOM.removeTabIndexAndUpdateFocus(this.domNode);
this.domNode.setAttribute('aria-disabled', String(true));
}
}
export class SimpleCheckbox extends Widget {
private checkbox: Checkbox;
private styles: ISimpleCheckboxStyles;
readonly domNode: HTMLElement;
constructor(private title: string, private isChecked: boolean) {
super();
this.checkbox = new Checkbox({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-simple-checkbox' });
this.domNode = this.checkbox.domNode;
this.styles = {};
this.checkbox.onChange(() => {
this.applyStyles();
});
}
get checked(): boolean {
return this.checkbox.checked;
}
set checked(newIsChecked: boolean) {
this.checkbox.checked = newIsChecked;
this.applyStyles();
}
focus(): void {
this.domNode.focus();
}
hasFocus(): boolean {
return this.domNode === document.activeElement;
}
style(styles: ISimpleCheckboxStyles): void {
this.styles = styles;
this.applyStyles();
}
protected applyStyles(): void {
this.domNode.style.color = this.styles.checkboxForeground ? this.styles.checkboxForeground.toString() : '';
this.domNode.style.backgroundColor = this.styles.checkboxBackground ? this.styles.checkboxBackground.toString() : '';
this.domNode.style.borderColor = this.styles.checkboxBorder ? this.styles.checkboxBorder.toString() : '';
}
}

View File

@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
@keyframes codicon-spin {
100% {
transform:rotate(360deg);
}
}
.codicon-animation-spin {
/* Use steps to throttle FPS to reduce CPU usage */
animation: codicon-spin 1.5s steps(30) infinite;
}

View File

@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.codicon-wrench-subaction {
opacity: 0.5;
}

View File

@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
@font-face {
font-family: "codicon";
src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype");
}
.codicon[class*='codicon-'] {
font: normal normal normal 16px/1 codicon;
display: inline-block;
text-decoration: none;
text-rendering: auto;
text-align: center;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
/* icon rules are dynamically created in codiconStyles */

View File

@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { reset } from 'vs/base/browser/dom';
import { renderCodicons } from 'vs/base/browser/codicons';
export class CodiconLabel {
constructor(
private readonly _container: HTMLElement
) { }
set text(text: string) {
reset(this._container, ...renderCodicons(text ?? ''));
}
set title(title: string) {
this._container.title = title;
}
}

View File

@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./codicon/codicon';
import 'vs/css!./codicon/codicon-modifications';
import 'vs/css!./codicon/codicon-animations';
import { Codicon, iconRegistry } from 'vs/base/common/codicons';
export const CodiconStyles = new class {
onDidChange = iconRegistry.onDidRegister;
public getCSS(): string {
const rules = [];
for (let c of iconRegistry.all) {
rules.push(formatRule(c));
}
return rules.join('\n');
}
};
export function formatRule(c: Codicon) {
let def = c.definition;
while (def instanceof Codicon) {
def = def.definition;
}
return `.codicon-${c.id}:before { content: '${def.character}'; }`;
}

View File

@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.context-view {
position: absolute;
z-index: 2500;
}
.context-view.fixed {
all: initial;
font-family: inherit;
font-size: 13px;
position: fixed;
z-index: 2500;
color: inherit;
}

View File

@ -0,0 +1,385 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./contextview';
import * as DOM from 'vs/base/browser/dom';
import * as platform from 'vs/base/common/platform';
import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Range } from 'vs/base/common/range';
import { BrowserFeatures } from 'vs/base/browser/canIUse';
export const enum ContextViewDOMPosition {
ABSOLUTE = 1,
FIXED,
FIXED_SHADOW
}
export interface IAnchor {
x: number;
y: number;
width?: number;
height?: number;
}
export const enum AnchorAlignment {
LEFT, RIGHT
}
export const enum AnchorPosition {
BELOW, ABOVE
}
export interface IDelegate {
getAnchor(): HTMLElement | IAnchor;
render(container: HTMLElement): IDisposable | null;
focus?(): void;
layout?(): void;
anchorAlignment?: AnchorAlignment; // default: left
anchorPosition?: AnchorPosition; // default: below
canRelayout?: boolean; // default: true
onDOMEvent?(e: Event, activeElement: HTMLElement): void;
onHide?(data?: any): void;
}
export interface IContextViewProvider {
showContextView(delegate: IDelegate, container?: HTMLElement): void;
hideContextView(): void;
layout(): void;
}
export interface IPosition {
top: number;
left: number;
}
export interface ISize {
width: number;
height: number;
}
export interface IView extends IPosition, ISize { }
export const enum LayoutAnchorPosition {
Before,
After
}
export interface ILayoutAnchor {
offset: number;
size: number;
position: LayoutAnchorPosition;
}
/**
* Lays out a one dimensional view next to an anchor in a viewport.
*
* @returns The view offset within the viewport.
*/
export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): number {
const anchorEnd = anchor.offset + anchor.size;
if (anchor.position === LayoutAnchorPosition.Before) {
if (viewSize <= viewportSize - anchorEnd) {
return anchorEnd; // happy case, lay it out after the anchor
}
if (viewSize <= anchor.offset) {
return anchor.offset - viewSize; // ok case, lay it out before the anchor
}
return Math.max(viewportSize - viewSize, 0); // sad case, lay it over the anchor
} else {
if (viewSize <= anchor.offset) {
return anchor.offset - viewSize; // happy case, lay it out before the anchor
}
if (viewSize <= viewportSize - anchorEnd) {
return anchorEnd; // ok case, lay it out after the anchor
}
return 0; // sad case, lay it over the anchor
}
}
export class ContextView extends Disposable {
private static readonly BUBBLE_UP_EVENTS = ['click', 'keydown', 'focus', 'blur'];
private static readonly BUBBLE_DOWN_EVENTS = ['click'];
private container: HTMLElement | null = null;
private view: HTMLElement;
private useFixedPosition: boolean;
private useShadowDOM: boolean;
private delegate: IDelegate | null = null;
private toDisposeOnClean: IDisposable = Disposable.None;
private toDisposeOnSetContainer: IDisposable = Disposable.None;
private shadowRoot: ShadowRoot | null = null;
private shadowRootHostElement: HTMLElement | null = null;
constructor(container: HTMLElement, domPosition: ContextViewDOMPosition) {
super();
this.view = DOM.$('.context-view');
this.useFixedPosition = false;
this.useShadowDOM = false;
DOM.hide(this.view);
this.setContainer(container, domPosition);
this._register(toDisposable(() => this.setContainer(null, ContextViewDOMPosition.ABSOLUTE)));
}
setContainer(container: HTMLElement | null, domPosition: ContextViewDOMPosition): void {
if (this.container) {
this.toDisposeOnSetContainer.dispose();
if (this.shadowRoot) {
this.shadowRoot.removeChild(this.view);
this.shadowRoot = null;
this.shadowRootHostElement?.remove();
this.shadowRootHostElement = null;
} else {
this.container.removeChild(this.view);
}
this.container = null;
}
if (container) {
this.container = container;
this.useFixedPosition = domPosition !== ContextViewDOMPosition.ABSOLUTE;
this.useShadowDOM = domPosition === ContextViewDOMPosition.FIXED_SHADOW;
if (this.useShadowDOM) {
this.shadowRootHostElement = DOM.$('.shadow-root-host');
this.container.appendChild(this.shadowRootHostElement);
this.shadowRoot = this.shadowRootHostElement.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = SHADOW_ROOT_CSS;
this.shadowRoot.appendChild(style);
this.shadowRoot.appendChild(this.view);
this.shadowRoot.appendChild(DOM.$('slot'));
} else {
this.container.appendChild(this.view);
}
const toDisposeOnSetContainer = new DisposableStore();
ContextView.BUBBLE_UP_EVENTS.forEach(event => {
toDisposeOnSetContainer.add(DOM.addStandardDisposableListener(this.container!, event, (e: Event) => {
this.onDOMEvent(e, false);
}));
});
ContextView.BUBBLE_DOWN_EVENTS.forEach(event => {
toDisposeOnSetContainer.add(DOM.addStandardDisposableListener(this.container!, event, (e: Event) => {
this.onDOMEvent(e, true);
}, true));
});
this.toDisposeOnSetContainer = toDisposeOnSetContainer;
}
}
show(delegate: IDelegate): void {
if (this.isVisible()) {
this.hide();
}
// Show static box
DOM.clearNode(this.view);
this.view.className = 'context-view';
this.view.style.top = '0px';
this.view.style.left = '0px';
this.view.style.zIndex = '2500';
this.view.style.position = this.useFixedPosition ? 'fixed' : 'absolute';
DOM.show(this.view);
// Render content
this.toDisposeOnClean = delegate.render(this.view) || Disposable.None;
// Set active delegate
this.delegate = delegate;
// Layout
this.doLayout();
// Focus
if (this.delegate.focus) {
this.delegate.focus();
}
}
getViewElement(): HTMLElement {
return this.view;
}
layout(): void {
if (!this.isVisible()) {
return;
}
if (this.delegate!.canRelayout === false && !(platform.isIOS && BrowserFeatures.pointerEvents)) {
this.hide();
return;
}
if (this.delegate!.layout) {
this.delegate!.layout!();
}
this.doLayout();
}
private doLayout(): void {
// Check that we still have a delegate - this.delegate.layout may have hidden
if (!this.isVisible()) {
return;
}
// Get anchor
let anchor = this.delegate!.getAnchor();
// Compute around
let around: IView;
// Get the element's position and size (to anchor the view)
if (DOM.isHTMLElement(anchor)) {
let elementPosition = DOM.getDomNodePagePosition(anchor);
around = {
top: elementPosition.top,
left: elementPosition.left,
width: elementPosition.width,
height: elementPosition.height
};
} else {
around = {
top: anchor.y,
left: anchor.x,
width: anchor.width || 1,
height: anchor.height || 2
};
}
const viewSizeWidth = DOM.getTotalWidth(this.view);
const viewSizeHeight = DOM.getTotalHeight(this.view);
const anchorPosition = this.delegate!.anchorPosition || AnchorPosition.BELOW;
const anchorAlignment = this.delegate!.anchorAlignment || AnchorAlignment.LEFT;
const verticalAnchor: ILayoutAnchor = { offset: around.top - window.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
let horizontalAnchor: ILayoutAnchor;
if (anchorAlignment === AnchorAlignment.LEFT) {
horizontalAnchor = { offset: around.left, size: 0, position: LayoutAnchorPosition.Before };
} else {
horizontalAnchor = { offset: around.left + around.width, size: 0, position: LayoutAnchorPosition.After };
}
const top = layout(window.innerHeight, viewSizeHeight, verticalAnchor) + window.pageYOffset;
// if view intersects vertically with anchor, shift it horizontally
if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
horizontalAnchor.size = around.width;
if (anchorAlignment === AnchorAlignment.RIGHT) {
horizontalAnchor.offset = around.left;
}
}
const left = layout(window.innerWidth, viewSizeWidth, horizontalAnchor);
this.view.classList.remove('top', 'bottom', 'left', 'right');
this.view.classList.add(anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top');
this.view.classList.add(anchorAlignment === AnchorAlignment.LEFT ? 'left' : 'right');
this.view.classList.toggle('fixed', this.useFixedPosition);
const containerPosition = DOM.getDomNodePagePosition(this.container!);
this.view.style.top = `${top - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).top : containerPosition.top)}px`;
this.view.style.left = `${left - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).left : containerPosition.left)}px`;
this.view.style.width = 'initial';
}
hide(data?: any): void {
const delegate = this.delegate;
this.delegate = null;
if (delegate?.onHide) {
delegate.onHide(data);
}
this.toDisposeOnClean.dispose();
DOM.hide(this.view);
}
private isVisible(): boolean {
return !!this.delegate;
}
private onDOMEvent(e: Event, onCapture: boolean): void {
if (this.delegate) {
if (this.delegate.onDOMEvent) {
this.delegate.onDOMEvent(e, <HTMLElement>document.activeElement);
} else if (onCapture && !DOM.isAncestor(<HTMLElement>e.target, this.container)) {
this.hide();
}
}
}
dispose(): void {
this.hide();
super.dispose();
}
}
let SHADOW_ROOT_CSS = /* css */ `
:host {
all: initial; /* 1st rule so subsequent properties are reset. */
}
@font-face {
font-family: "codicon";
src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype");
}
.codicon[class*='codicon-'] {
font: normal normal normal 16px/1 codicon;
display: inline-block;
text-decoration: none;
text-rendering: auto;
text-align: center;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
:host {
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", system-ui, "Ubuntu", "Droid Sans", sans-serif;
}
:host-context(.mac) { font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
:host-context(.mac:lang(zh-Hans)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; }
:host-context(.mac:lang(zh-Hant)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; }
:host-context(.mac:lang(ja)) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; }
:host-context(.mac:lang(ko)) { font-family: -apple-system, BlinkMacSystemFont, "Nanum Gothic", "Apple SD Gothic Neo", "AppleGothic", sans-serif; }
:host-context(.windows) { font-family: "Segoe WPC", "Segoe UI", sans-serif; }
:host-context(.windows:lang(zh-Hans)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; }
:host-context(.windows:lang(zh-Hant)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; }
:host-context(.windows:lang(ja)) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; }
:host-context(.windows:lang(ko)) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; }
:host-context(.linux) { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; }
:host-context(.linux:lang(zh-Hans)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; }
:host-context(.linux:lang(zh-Hant)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; }
:host-context(.linux:lang(ja)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; }
:host-context(.linux:lang(ko)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; }
`;

View File

@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-count-badge {
padding: 3px 6px;
border-radius: 11px;
font-size: 11px;
min-width: 18px;
min-height: 18px;
line-height: 11px;
font-weight: normal;
text-align: center;
display: inline-block;
box-sizing: border-box;
}
.monaco-count-badge.long {
padding: 2px 3px;
border-radius: 2px;
min-height: auto;
line-height: normal;
}

View File

@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./countBadge';
import { $, append } from 'vs/base/browser/dom';
import { format } from 'vs/base/common/strings';
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
import { IThemable } from 'vs/base/common/styler';
export interface ICountBadgeOptions extends ICountBadgetyles {
count?: number;
countFormat?: string;
titleFormat?: string;
}
export interface ICountBadgetyles {
badgeBackground?: Color;
badgeForeground?: Color;
badgeBorder?: Color;
}
const defaultOpts = {
badgeBackground: Color.fromHex('#4D4D4D'),
badgeForeground: Color.fromHex('#FFFFFF')
};
export class CountBadge implements IThemable {
private element: HTMLElement;
private count: number = 0;
private countFormat: string;
private titleFormat: string;
private badgeBackground: Color | undefined;
private badgeForeground: Color | undefined;
private badgeBorder: Color | undefined;
private options: ICountBadgeOptions;
constructor(container: HTMLElement, options?: ICountBadgeOptions) {
this.options = options || Object.create(null);
mixin(this.options, defaultOpts, false);
this.badgeBackground = this.options.badgeBackground;
this.badgeForeground = this.options.badgeForeground;
this.badgeBorder = this.options.badgeBorder;
this.element = append(container, $('.monaco-count-badge'));
this.countFormat = this.options.countFormat || '{0}';
this.titleFormat = this.options.titleFormat || '';
this.setCount(this.options.count || 0);
}
setCount(count: number) {
this.count = count;
this.render();
}
setCountFormat(countFormat: string) {
this.countFormat = countFormat;
this.render();
}
setTitleFormat(titleFormat: string) {
this.titleFormat = titleFormat;
this.render();
}
private render() {
this.element.textContent = format(this.countFormat, this.count);
this.element.title = format(this.titleFormat, this.count);
this.applyStyles();
}
style(styles: ICountBadgetyles): void {
this.badgeBackground = styles.badgeBackground;
this.badgeForeground = styles.badgeForeground;
this.badgeBorder = styles.badgeBorder;
this.applyStyles();
}
private applyStyles(): void {
if (this.element) {
const background = this.badgeBackground ? this.badgeBackground.toString() : '';
const foreground = this.badgeForeground ? this.badgeForeground.toString() : '';
const border = this.badgeBorder ? this.badgeBorder.toString() : '';
this.element.style.backgroundColor = background;
this.element.style.color = foreground;
this.element.style.borderWidth = border ? '1px' : '';
this.element.style.borderStyle = border ? 'solid' : '';
this.element.style.borderColor = border;
}
}
}

View File

@ -0,0 +1,154 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/** Dialog: Modal Block */
.monaco-dialog-modal-block {
position: fixed;
height: 100%;
width: 100%;
left:0;
top:0;
z-index: 2000;
display: flex;
justify-content: center;
align-items: center;
}
.monaco-dialog-modal-block.dimmed {
background: rgba(0, 0, 0, 0.3);
}
/** Dialog: Container */
.monaco-dialog-box {
display: flex;
flex-direction: column-reverse;
width: min-content;
min-width: 500px;
max-width: 90vw;
min-height: 75px;
padding: 10px;
transform: translate3d(0px, 0px, 0px);
}
/** Dialog: Title Actions Row */
.monaco-dialog-box .dialog-toolbar-row {
padding-bottom: 4px;
}
.monaco-dialog-box .action-label {
height: 16px;
min-width: 16px;
background-size: 16px;
background-position: 50%;
background-repeat: no-repeat;
margin: 0px;
margin-left: 4px;
}
/** Dialog: Message Row */
.monaco-dialog-box .dialog-message-row {
display: flex;
flex-grow: 1;
align-items: center;
padding: 0 10px;
}
.monaco-dialog-box .dialog-message-row > .dialog-icon.codicon {
flex: 0 0 48px;
height: 48px;
align-self: baseline;
font-size: 48px;
}
/** Dialog: Message Container */
.monaco-dialog-box .dialog-message-row .dialog-message-container {
display: flex;
flex-direction: column;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 24px;
user-select: text;
-webkit-user-select: text;
-ms-user-select: text;
word-wrap: break-word; /* never overflow long words, but break to next line */
white-space: normal;
}
/** Dialog: Message */
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message {
line-height: 22px;
font-size: 18px;
flex: 1; /* let the message always grow */
white-space: normal;
word-wrap: break-word; /* never overflow long words, but break to next line */
min-height: 48px; /* matches icon height */
margin-bottom: 8px;
display: flex;
align-items: center;
}
/** Dialog: Details */
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-detail {
line-height: 22px;
flex: 1; /* let the message always grow */
}
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message a:focus {
outline-width: 1px;
outline-style: solid;
}
/** Dialog: Checkbox */
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-checkbox-row {
padding: 15px 0px 0px;
display: flex;
}
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-checkbox-row .dialog-checkbox-message {
cursor: pointer;
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
/** Dialog: Input */
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-input {
padding: 15px 0px 0px;
display: flex;
}
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-input .monaco-inputbox {
flex: 1;
}
/** Dialog: Buttons Row */
.monaco-dialog-box > .dialog-buttons-row {
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 1px;
overflow: hidden; /* buttons row should never overflow */
}
.monaco-dialog-box > .dialog-buttons-row {
display: flex;
white-space: nowrap;
padding: 20px 10px 10px;
}
/** Dialog: Buttons */
.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons {
display: flex;
overflow: hidden;
}
.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button {
width: fit-content;
width: -moz-fit-content;
padding: 5px 10px;
margin: 4px 5px; /* allows button focus outline to be visible */
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -0,0 +1,448 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./dialog';
import * as nls from 'vs/nls';
import { Disposable } from 'vs/base/common/lifecycle';
import { $, hide, show, EventHelper, clearNode, isAncestor, addDisposableListener, EventType } from 'vs/base/browser/dom';
import { domEvent } from 'vs/base/browser/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Color } from 'vs/base/common/color';
import { ButtonGroup, IButtonStyles } from 'vs/base/browser/ui/button/button';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action } from 'vs/base/common/actions';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { isMacintosh, isLinux } from 'vs/base/common/platform';
import { SimpleCheckbox, ISimpleCheckboxStyles } from 'vs/base/browser/ui/checkbox/checkbox';
import { Codicon, registerIcon } from 'vs/base/common/codicons';
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
export interface IDialogInputOptions {
readonly placeholder?: string;
readonly type?: 'text' | 'password';
readonly value?: string;
}
export interface IDialogOptions {
readonly cancelId?: number;
readonly detail?: string;
readonly checkboxLabel?: string;
readonly checkboxChecked?: boolean;
readonly type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending';
readonly inputs?: IDialogInputOptions[];
readonly keyEventProcessor?: (event: StandardKeyboardEvent) => void;
}
export interface IDialogResult {
readonly button: number;
readonly checkboxChecked?: boolean;
readonly values?: string[];
}
export interface IDialogStyles extends IButtonStyles, ISimpleCheckboxStyles {
readonly dialogForeground?: Color;
readonly dialogBackground?: Color;
readonly dialogShadow?: Color;
readonly dialogBorder?: Color;
readonly errorIconForeground?: Color;
readonly warningIconForeground?: Color;
readonly infoIconForeground?: Color;
readonly inputBackground?: Color;
readonly inputForeground?: Color;
readonly inputBorder?: Color;
}
interface ButtonMapEntry {
readonly label: string;
readonly index: number;
}
const dialogErrorIcon = registerIcon('dialog-error', Codicon.error);
const dialogWarningIcon = registerIcon('dialog-warning', Codicon.warning);
const dialogInfoIcon = registerIcon('dialog-info', Codicon.info);
const dialogCloseIcon = registerIcon('dialog-close', Codicon.close);
export class Dialog extends Disposable {
private readonly element: HTMLElement;
private readonly shadowElement: HTMLElement;
private modalElement: HTMLElement | undefined;
private readonly buttonsContainer: HTMLElement;
private readonly messageDetailElement: HTMLElement;
private readonly iconElement: HTMLElement;
private readonly checkbox: SimpleCheckbox | undefined;
private readonly toolbarContainer: HTMLElement;
private buttonGroup: ButtonGroup | undefined;
private styles: IDialogStyles | undefined;
private focusToReturn: HTMLElement | undefined;
private readonly inputs: InputBox[];
private readonly buttons: string[];
constructor(private container: HTMLElement, private message: string, buttons: string[], private options: IDialogOptions) {
super();
this.modalElement = this.container.appendChild($(`.monaco-dialog-modal-block${options.type === 'pending' ? '.dimmed' : ''}`));
this.shadowElement = this.modalElement.appendChild($('.dialog-shadow'));
this.element = this.shadowElement.appendChild($('.monaco-dialog-box'));
this.element.setAttribute('role', 'dialog');
hide(this.element);
this.buttons = buttons.length ? buttons : [nls.localize('ok', "OK")]; // If no button is provided, default to OK
const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row'));
this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons'));
const messageRowElement = this.element.appendChild($('.dialog-message-row'));
this.iconElement = messageRowElement.appendChild($('.dialog-icon'));
const messageContainer = messageRowElement.appendChild($('.dialog-message-container'));
if (this.options.detail) {
const messageElement = messageContainer.appendChild($('.dialog-message'));
const messageTextElement = messageElement.appendChild($('.dialog-message-text'));
messageTextElement.innerText = this.message;
}
this.messageDetailElement = messageContainer.appendChild($('.dialog-message-detail'));
this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message;
if (this.options.inputs) {
this.inputs = this.options.inputs.map(input => {
const inputRowElement = messageContainer.appendChild($('.dialog-message-input'));
const inputBox = this._register(new InputBox(inputRowElement, undefined, {
placeholder: input.placeholder,
type: input.type ?? 'text',
}));
if (input.value) {
inputBox.value = input.value;
}
return inputBox;
});
} else {
this.inputs = [];
}
if (this.options.checkboxLabel) {
const checkboxRowElement = messageContainer.appendChild($('.dialog-checkbox-row'));
const checkbox = this.checkbox = this._register(new SimpleCheckbox(this.options.checkboxLabel, !!this.options.checkboxChecked));
checkboxRowElement.appendChild(checkbox.domNode);
const checkboxMessageElement = checkboxRowElement.appendChild($('.dialog-checkbox-message'));
checkboxMessageElement.innerText = this.options.checkboxLabel;
this._register(addDisposableListener(checkboxMessageElement, EventType.CLICK, () => checkbox.checked = !checkbox.checked));
}
const toolbarRowElement = this.element.appendChild($('.dialog-toolbar-row'));
this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar'));
}
private getAriaLabel(): string {
let typeLabel = nls.localize('dialogInfoMessage', 'Info');
switch (this.options.type) {
case 'error':
nls.localize('dialogErrorMessage', 'Error');
break;
case 'warning':
nls.localize('dialogWarningMessage', 'Warning');
break;
case 'pending':
nls.localize('dialogPendingMessage', 'In Progress');
break;
case 'none':
case 'info':
case 'question':
default:
break;
}
return `${typeLabel}: ${this.message} ${this.options.detail || ''}`;
}
updateMessage(message: string): void {
this.messageDetailElement.innerText = message;
}
async show(): Promise<IDialogResult> {
this.focusToReturn = document.activeElement as HTMLElement;
return new Promise<IDialogResult>((resolve) => {
clearNode(this.buttonsContainer);
const buttonGroup = this.buttonGroup = this._register(new ButtonGroup(this.buttonsContainer, this.buttons.length, { title: true }));
const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId);
// Handle button clicks
buttonGroup.buttons.forEach((button, index) => {
button.label = mnemonicButtonLabel(buttonMap[index].label, true);
this._register(button.onDidClick(e => {
EventHelper.stop(e);
resolve({
button: buttonMap[index].index,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
});
}));
});
// Handle keyboard events gloably: Tab, Arrow-Left/Right
this._register(domEvent(window, 'keydown', true)((e: KeyboardEvent) => {
const evt = new StandardKeyboardEvent(e);
if (evt.equals(KeyCode.Enter)) {
// Enter in input field should OK the dialog
if (this.inputs.some(input => input.hasFocus())) {
EventHelper.stop(e);
resolve({
button: buttonMap.find(button => button.index !== this.options.cancelId)?.index ?? 0,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
});
}
return; // leave default handling
}
if (evt.equals(KeyCode.Space)) {
return; // leave default handling
}
let eventHandled = false;
// Focus: Next / Previous
if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow) || evt.equals(KeyMod.Shift | KeyCode.Tab) || evt.equals(KeyCode.LeftArrow)) {
// Build a list of focusable elements in their visual order
const focusableElements: { focus: () => void }[] = [];
let focusedIndex = -1;
for (const input of this.inputs) {
focusableElements.push(input);
if (input.hasFocus()) {
focusedIndex = focusableElements.length - 1;
}
}
if (this.checkbox) {
focusableElements.push(this.checkbox);
if (this.checkbox.hasFocus()) {
focusedIndex = focusableElements.length - 1;
}
}
if (this.buttonGroup) {
for (const button of this.buttonGroup.buttons) {
focusableElements.push(button);
if (button.hasFocus()) {
focusedIndex = focusableElements.length - 1;
}
}
}
// Focus next element (with wrapping)
if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) {
if (focusedIndex === -1) {
focusedIndex = 0; // default to focus first element if none have focus
}
const newFocusedIndex = (focusedIndex + 1) % focusableElements.length;
focusableElements[newFocusedIndex].focus();
}
// Focus previous element (with wrapping)
else {
if (focusedIndex === -1) {
focusedIndex = focusableElements.length; // default to focus last element if none have focus
}
let newFocusedIndex = focusedIndex - 1;
if (newFocusedIndex === -1) {
newFocusedIndex = focusableElements.length - 1;
}
focusableElements[newFocusedIndex].focus();
}
eventHandled = true;
}
if (eventHandled) {
EventHelper.stop(e, true);
} else if (this.options.keyEventProcessor) {
this.options.keyEventProcessor(evt);
}
}));
this._register(domEvent(window, 'keyup', true)((e: KeyboardEvent) => {
EventHelper.stop(e, true);
const evt = new StandardKeyboardEvent(e);
if (evt.equals(KeyCode.Escape)) {
resolve({
button: this.options.cancelId || 0,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
});
}
}));
// Detect focus out
this._register(domEvent(this.element, 'focusout', false)((e: FocusEvent) => {
if (!!e.relatedTarget && !!this.element) {
if (!isAncestor(e.relatedTarget as HTMLElement, this.element)) {
this.focusToReturn = e.relatedTarget as HTMLElement;
if (e.target) {
(e.target as HTMLElement).focus();
EventHelper.stop(e, true);
}
}
}
}));
this.iconElement.classList.remove(...dialogErrorIcon.classNamesArray, ...dialogWarningIcon.classNamesArray, ...dialogInfoIcon.classNamesArray, ...Codicon.loading.classNamesArray);
switch (this.options.type) {
case 'error':
this.iconElement.classList.add(...dialogErrorIcon.classNamesArray);
break;
case 'warning':
this.iconElement.classList.add(...dialogWarningIcon.classNamesArray);
break;
case 'pending':
this.iconElement.classList.add(...Codicon.loading.classNamesArray, 'codicon-animation-spin');
break;
case 'none':
case 'info':
case 'question':
default:
this.iconElement.classList.add(...dialogInfoIcon.classNamesArray);
break;
}
const actionBar = this._register(new ActionBar(this.toolbarContainer, {}));
const action = this._register(new Action('dialog.close', nls.localize('dialogClose', "Close Dialog"), dialogCloseIcon.classNames, true, async () => {
resolve({
button: this.options.cancelId || 0,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
});
}));
actionBar.push(action, { icon: true, label: false, });
this.applyStyles();
this.element.setAttribute('aria-label', this.getAriaLabel());
show(this.element);
// Focus first element (input or button)
if (this.inputs.length > 0) {
this.inputs[0].focus();
this.inputs[0].select();
} else {
buttonMap.forEach((value, index) => {
if (value.index === 0) {
buttonGroup.buttons[index].focus();
}
});
}
});
}
private applyStyles() {
if (this.styles) {
const style = this.styles;
const fgColor = style.dialogForeground;
const bgColor = style.dialogBackground;
const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : '';
const border = style.dialogBorder ? `1px solid ${style.dialogBorder}` : '';
this.shadowElement.style.boxShadow = shadowColor;
this.element.style.color = fgColor?.toString() ?? '';
this.element.style.backgroundColor = bgColor?.toString() ?? '';
this.element.style.border = border;
if (this.buttonGroup) {
this.buttonGroup.buttons.forEach(button => button.style(style));
}
if (this.checkbox) {
this.checkbox.style(style);
}
if (fgColor && bgColor) {
const messageDetailColor = fgColor.transparent(.9);
this.messageDetailElement.style.color = messageDetailColor.makeOpaque(bgColor).toString();
}
let color;
switch (this.options.type) {
case 'error':
color = style.errorIconForeground;
break;
case 'warning':
color = style.warningIconForeground;
break;
default:
color = style.infoIconForeground;
break;
}
if (color) {
this.iconElement.style.color = color.toString();
}
for (const input of this.inputs) {
input.style(style);
}
}
}
style(style: IDialogStyles): void {
this.styles = style;
this.applyStyles();
}
dispose(): void {
super.dispose();
if (this.modalElement) {
this.modalElement.remove();
this.modalElement = undefined;
}
if (this.focusToReturn && isAncestor(this.focusToReturn, document.body)) {
this.focusToReturn.focus();
this.focusToReturn = undefined;
}
}
private rearrangeButtons(buttons: Array<string>, cancelId: number | undefined): ButtonMapEntry[] {
const buttonMap: ButtonMapEntry[] = [];
// Maps each button to its current label and old index so that when we move them around it's not a problem
buttons.forEach((button, index) => {
buttonMap.push({ label: button, index });
});
// macOS/linux: reverse button order
if (isMacintosh || isLinux) {
if (cancelId !== undefined) {
const cancelButton = buttonMap.splice(cancelId, 1)[0];
buttonMap.reverse();
buttonMap.splice(buttonMap.length - 1, 0, cancelButton);
}
}
return buttonMap;
}
}

View File

@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-dropdown {
height: 100%;
padding: 0;
}
.monaco-dropdown > .dropdown-label {
cursor: pointer;
height: 100%;
}

View File

@ -0,0 +1,280 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./dropdown';
import { Gesture, EventType as GestureEventType } from 'vs/base/browser/touch';
import { ActionRunner, IAction } from 'vs/base/common/actions';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IContextViewProvider, IAnchor, AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { IMenuOptions } from 'vs/base/browser/ui/menu/menu';
import { KeyCode } from 'vs/base/common/keyCodes';
import { EventHelper, EventType, append, $, addDisposableListener, DOMEvent } from 'vs/base/browser/dom';
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Emitter } from 'vs/base/common/event';
export interface ILabelRenderer {
(container: HTMLElement): IDisposable | null;
}
export interface IBaseDropdownOptions {
label?: string;
labelRenderer?: ILabelRenderer;
}
export class BaseDropdown extends ActionRunner {
private _element: HTMLElement;
private boxContainer?: HTMLElement;
private _label?: HTMLElement;
private contents?: HTMLElement;
private visible: boolean | undefined;
private _onDidChangeVisibility = new Emitter<boolean>();
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
constructor(container: HTMLElement, options: IBaseDropdownOptions) {
super();
this._element = append(container, $('.monaco-dropdown'));
this._label = append(this._element, $('.dropdown-label'));
let labelRenderer = options.labelRenderer;
if (!labelRenderer) {
labelRenderer = (container: HTMLElement): IDisposable | null => {
container.textContent = options.label || '';
return null;
};
}
for (const event of [EventType.CLICK, EventType.MOUSE_DOWN, GestureEventType.Tap]) {
this._register(addDisposableListener(this.element, event, e => EventHelper.stop(e, true))); // prevent default click behaviour to trigger
}
for (const event of [EventType.MOUSE_DOWN, GestureEventType.Tap]) {
this._register(addDisposableListener(this._label, event, e => {
if (e instanceof MouseEvent && e.detail > 1) {
return; // prevent multiple clicks to open multiple context menus (https://github.com/microsoft/vscode/issues/41363)
}
if (this.visible) {
this.hide();
} else {
this.show();
}
}));
}
this._register(addDisposableListener(this._label, EventType.KEY_UP, e => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
EventHelper.stop(e, true); // https://github.com/microsoft/vscode/issues/57997
if (this.visible) {
this.hide();
} else {
this.show();
}
}
}));
const cleanupFn = labelRenderer(this._label);
if (cleanupFn) {
this._register(cleanupFn);
}
this._register(Gesture.addTarget(this._label));
}
get element(): HTMLElement {
return this._element;
}
get label() {
return this._label;
}
set tooltip(tooltip: string) {
if (this._label) {
this._label.title = tooltip;
}
}
show(): void {
if (!this.visible) {
this.visible = true;
this._onDidChangeVisibility.fire(true);
}
}
hide(): void {
if (this.visible) {
this.visible = false;
this._onDidChangeVisibility.fire(false);
}
}
isVisible(): boolean {
return !!this.visible;
}
protected onEvent(e: DOMEvent, activeElement: HTMLElement): void {
this.hide();
}
dispose(): void {
super.dispose();
this.hide();
if (this.boxContainer) {
this.boxContainer.remove();
this.boxContainer = undefined;
}
if (this.contents) {
this.contents.remove();
this.contents = undefined;
}
if (this._label) {
this._label.remove();
this._label = undefined;
}
}
}
export interface IDropdownOptions extends IBaseDropdownOptions {
contextViewProvider: IContextViewProvider;
}
export class Dropdown extends BaseDropdown {
private contextViewProvider: IContextViewProvider;
constructor(container: HTMLElement, options: IDropdownOptions) {
super(container, options);
this.contextViewProvider = options.contextViewProvider;
}
show(): void {
super.show();
this.element.classList.add('active');
this.contextViewProvider.showContextView({
getAnchor: () => this.getAnchor(),
render: (container) => {
return this.renderContents(container);
},
onDOMEvent: (e, activeElement) => {
this.onEvent(e, activeElement);
},
onHide: () => this.onHide()
});
}
protected getAnchor(): HTMLElement | IAnchor {
return this.element;
}
protected onHide(): void {
this.element.classList.remove('active');
}
hide(): void {
super.hide();
if (this.contextViewProvider) {
this.contextViewProvider.hideContextView();
}
}
protected renderContents(container: HTMLElement): IDisposable | null {
return null;
}
}
export interface IActionProvider {
getActions(): IAction[];
}
export interface IDropdownMenuOptions extends IBaseDropdownOptions {
contextMenuProvider: IContextMenuProvider;
readonly actions?: IAction[];
readonly actionProvider?: IActionProvider;
menuClassName?: string;
menuAsChild?: boolean; // scope down for #99448
}
export class DropdownMenu extends BaseDropdown {
private _contextMenuProvider: IContextMenuProvider;
private _menuOptions: IMenuOptions | undefined;
private _actions: IAction[] = [];
private actionProvider?: IActionProvider;
private menuClassName: string;
private menuAsChild?: boolean;
constructor(container: HTMLElement, options: IDropdownMenuOptions) {
super(container, options);
this._contextMenuProvider = options.contextMenuProvider;
this.actions = options.actions || [];
this.actionProvider = options.actionProvider;
this.menuClassName = options.menuClassName || '';
this.menuAsChild = !!options.menuAsChild;
}
set menuOptions(options: IMenuOptions | undefined) {
this._menuOptions = options;
}
get menuOptions(): IMenuOptions | undefined {
return this._menuOptions;
}
private get actions(): IAction[] {
if (this.actionProvider) {
return this.actionProvider.getActions();
}
return this._actions;
}
private set actions(actions: IAction[]) {
this._actions = actions;
}
show(): void {
super.show();
this.element.classList.add('active');
this._contextMenuProvider.showContextMenu({
getAnchor: () => this.element,
getActions: () => this.actions,
getActionsContext: () => this.menuOptions ? this.menuOptions.context : null,
getActionViewItem: action => this.menuOptions && this.menuOptions.actionViewItemProvider ? this.menuOptions.actionViewItemProvider(action) : undefined,
getKeyBinding: action => this.menuOptions && this.menuOptions.getKeyBinding ? this.menuOptions.getKeyBinding(action) : undefined,
getMenuClassName: () => this.menuClassName,
onHide: () => this.onHide(),
actionRunner: this.menuOptions ? this.menuOptions.actionRunner : undefined,
anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : AnchorAlignment.LEFT,
domForShadowRoot: this.menuAsChild ? this.element : undefined
});
}
hide(): void {
super.hide();
}
private onHide(): void {
this.hide();
this.element.classList.remove('active');
}
}

View File

@ -0,0 +1,166 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./dropdown';
import { Action, IAction, IActionRunner, IActionViewItemProvider } from 'vs/base/common/actions';
import { IDisposable } from 'vs/base/common/lifecycle';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { append, $ } from 'vs/base/browser/dom';
import { Emitter } from 'vs/base/common/event';
import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { IActionProvider, DropdownMenu, IDropdownMenuOptions, ILabelRenderer } from 'vs/base/browser/ui/dropdown/dropdown';
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
export interface IKeybindingProvider {
(action: IAction): ResolvedKeybinding | undefined;
}
export interface IAnchorAlignmentProvider {
(): AnchorAlignment;
}
export interface IDropdownMenuActionViewItemOptions extends IBaseActionViewItemOptions {
readonly actionViewItemProvider?: IActionViewItemProvider;
readonly keybindingProvider?: IKeybindingProvider;
readonly actionRunner?: IActionRunner;
readonly classNames?: string[] | string;
readonly anchorAlignmentProvider?: IAnchorAlignmentProvider;
readonly menuAsChild?: boolean;
}
export class DropdownMenuActionViewItem extends BaseActionViewItem {
private menuActionsOrProvider: readonly IAction[] | IActionProvider;
private dropdownMenu: DropdownMenu | undefined;
private contextMenuProvider: IContextMenuProvider;
private _onDidChangeVisibility = this._register(new Emitter<boolean>());
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
constructor(
action: IAction,
menuActionsOrProvider: readonly IAction[] | IActionProvider,
contextMenuProvider: IContextMenuProvider,
protected options: IDropdownMenuActionViewItemOptions = {}
) {
super(null, action, options);
this.menuActionsOrProvider = menuActionsOrProvider;
this.contextMenuProvider = contextMenuProvider;
if (this.options.actionRunner) {
this.actionRunner = this.options.actionRunner;
}
}
render(container: HTMLElement): void {
const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => {
this.element = append(el, $('a.action-label'));
let classNames: string[] = [];
if (typeof this.options.classNames === 'string') {
classNames = this.options.classNames.split(/\s+/g).filter(s => !!s);
} else if (this.options.classNames) {
classNames = this.options.classNames;
}
// todo@aeschli: remove codicon, should come through `this.options.classNames`
if (!classNames.find(c => c === 'icon')) {
classNames.push('codicon');
}
this.element.classList.add(...classNames);
this.element.tabIndex = 0;
this.element.setAttribute('role', 'button');
this.element.setAttribute('aria-haspopup', 'true');
this.element.setAttribute('aria-expanded', 'false');
this.element.title = this._action.label || '';
return null;
};
const isActionsArray = Array.isArray(this.menuActionsOrProvider);
const options: IDropdownMenuOptions = {
contextMenuProvider: this.contextMenuProvider,
labelRenderer: labelRenderer,
menuAsChild: this.options.menuAsChild,
actions: isActionsArray ? this.menuActionsOrProvider as IAction[] : undefined,
actionProvider: isActionsArray ? undefined : this.menuActionsOrProvider as IActionProvider
};
this.dropdownMenu = this._register(new DropdownMenu(container, options));
this._register(this.dropdownMenu.onDidChangeVisibility(visible => {
this.element?.setAttribute('aria-expanded', `${visible}`);
this._onDidChangeVisibility.fire(visible);
}));
this.dropdownMenu.menuOptions = {
actionViewItemProvider: this.options.actionViewItemProvider,
actionRunner: this.actionRunner,
getKeyBinding: this.options.keybindingProvider,
context: this._context
};
if (this.options.anchorAlignmentProvider) {
const that = this;
this.dropdownMenu.menuOptions = {
...this.dropdownMenu.menuOptions,
get anchorAlignment(): AnchorAlignment {
return that.options.anchorAlignmentProvider!();
}
};
}
}
setActionContext(newContext: unknown): void {
super.setActionContext(newContext);
if (this.dropdownMenu) {
if (this.dropdownMenu.menuOptions) {
this.dropdownMenu.menuOptions.context = newContext;
} else {
this.dropdownMenu.menuOptions = { context: newContext };
}
}
}
show(): void {
if (this.dropdownMenu) {
this.dropdownMenu.show();
}
}
}
export interface IActionWithDropdownActionViewItemOptions extends IActionViewItemOptions {
readonly menuActionsOrProvider: readonly IAction[] | IActionProvider;
readonly menuActionClassNames?: string[];
}
export class ActionWithDropdownActionViewItem extends ActionViewItem {
protected dropdownMenuActionViewItem: DropdownMenuActionViewItem | undefined;
constructor(
context: unknown,
action: IAction,
options: IActionWithDropdownActionViewItemOptions,
private readonly contextMenuProvider: IContextMenuProvider
) {
super(context, action, options);
}
render(container: HTMLElement): void {
super.render(container);
if (this.element) {
this.element.classList.add('action-dropdown-item');
this.dropdownMenuActionViewItem = new DropdownMenuActionViewItem(new Action('dropdownAction', undefined), (<IActionWithDropdownActionViewItemOptions>this.options).menuActionsOrProvider, this.contextMenuProvider, { classNames: ['dropdown', 'codicon-chevron-down', ...(<IActionWithDropdownActionViewItemOptions>this.options).menuActionClassNames || []] });
this.dropdownMenuActionViewItem.render(this.element);
}
}
}

View File

@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* ---------- Find input ---------- */
.monaco-findInput {
position: relative;
}
.monaco-findInput .monaco-inputbox {
font-size: 13px;
width: 100%;
}
.monaco-findInput > .controls {
position: absolute;
top: 3px;
right: 2px;
}
.vs .monaco-findInput.disabled {
background-color: #E1E1E1;
}
/* Theming */
.vs-dark .monaco-findInput.disabled {
background-color: #333;
}
/* Highlighting */
.monaco-findInput.highlight-0 .controls {
animation: monaco-findInput-highlight-0 100ms linear 0s;
}
.monaco-findInput.highlight-1 .controls {
animation: monaco-findInput-highlight-1 100ms linear 0s;
}
.hc-black .monaco-findInput.highlight-0 .controls,
.vs-dark .monaco-findInput.highlight-0 .controls {
animation: monaco-findInput-highlight-dark-0 100ms linear 0s;
}
.hc-black .monaco-findInput.highlight-1 .controls,
.vs-dark .monaco-findInput.highlight-1 .controls {
animation: monaco-findInput-highlight-dark-1 100ms linear 0s;
}
@keyframes monaco-findInput-highlight-0 {
0% { background: rgba(253, 255, 0, 0.8); }
100% { background: transparent; }
}
@keyframes monaco-findInput-highlight-1 {
0% { background: rgba(253, 255, 0, 0.8); }
/* Made intentionally different such that the CSS minifier does not collapse the two animations into a single one*/
99% { background: transparent; }
}
@keyframes monaco-findInput-highlight-dark-0 {
0% { background: rgba(255, 255, 255, 0.44); }
100% { background: transparent; }
}
@keyframes monaco-findInput-highlight-dark-1 {
0% { background: rgba(255, 255, 255, 0.44); }
/* Made intentionally different such that the CSS minifier does not collapse the two animations into a single one*/
99% { background: transparent; }
}

View File

@ -0,0 +1,422 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./findInput';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { IMessage as InputBoxMessage, IInputValidator, IInputBoxStyles, HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { Widget } from 'vs/base/browser/ui/widget';
import { Event, Emitter } from 'vs/base/common/event';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { CaseSensitiveCheckbox, WholeWordsCheckbox, RegexCheckbox } from 'vs/base/browser/ui/findinput/findInputCheckboxes';
import { Color } from 'vs/base/common/color';
import { ICheckboxStyles } from 'vs/base/browser/ui/checkbox/checkbox';
export interface IFindInputOptions extends IFindInputStyles {
readonly placeholder?: string;
readonly width?: number;
readonly validation?: IInputValidator;
readonly label: string;
readonly flexibleHeight?: boolean;
readonly flexibleWidth?: boolean;
readonly flexibleMaxHeight?: number;
readonly appendCaseSensitiveLabel?: string;
readonly appendWholeWordsLabel?: string;
readonly appendRegexLabel?: string;
readonly history?: string[];
}
export interface IFindInputStyles extends IInputBoxStyles {
inputActiveOptionBorder?: Color;
inputActiveOptionForeground?: Color;
inputActiveOptionBackground?: Color;
}
const NLS_DEFAULT_LABEL = nls.localize('defaultLabel', "input");
export class FindInput extends Widget {
static readonly OPTION_CHANGE: string = 'optionChange';
private contextViewProvider: IContextViewProvider;
private placeholder: string;
private validation?: IInputValidator;
private label: string;
private fixFocusOnOptionClickEnabled = true;
private inputActiveOptionBorder?: Color;
private inputActiveOptionForeground?: Color;
private inputActiveOptionBackground?: Color;
private inputBackground?: Color;
private inputForeground?: Color;
private inputBorder?: Color;
private inputValidationInfoBorder?: Color;
private inputValidationInfoBackground?: Color;
private inputValidationInfoForeground?: Color;
private inputValidationWarningBorder?: Color;
private inputValidationWarningBackground?: Color;
private inputValidationWarningForeground?: Color;
private inputValidationErrorBorder?: Color;
private inputValidationErrorBackground?: Color;
private inputValidationErrorForeground?: Color;
private regex: RegexCheckbox;
private wholeWords: WholeWordsCheckbox;
private caseSensitive: CaseSensitiveCheckbox;
public domNode: HTMLElement;
public inputBox: HistoryInputBox;
private readonly _onDidOptionChange = this._register(new Emitter<boolean>());
public readonly onDidOptionChange: Event<boolean /* via keyboard */> = this._onDidOptionChange.event;
private readonly _onKeyDown = this._register(new Emitter<IKeyboardEvent>());
public readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;
private readonly _onMouseDown = this._register(new Emitter<IMouseEvent>());
public readonly onMouseDown: Event<IMouseEvent> = this._onMouseDown.event;
private readonly _onInput = this._register(new Emitter<void>());
public readonly onInput: Event<void> = this._onInput.event;
private readonly _onKeyUp = this._register(new Emitter<IKeyboardEvent>());
public readonly onKeyUp: Event<IKeyboardEvent> = this._onKeyUp.event;
private _onCaseSensitiveKeyDown = this._register(new Emitter<IKeyboardEvent>());
public readonly onCaseSensitiveKeyDown: Event<IKeyboardEvent> = this._onCaseSensitiveKeyDown.event;
private _onRegexKeyDown = this._register(new Emitter<IKeyboardEvent>());
public readonly onRegexKeyDown: Event<IKeyboardEvent> = this._onRegexKeyDown.event;
constructor(parent: HTMLElement | null, contextViewProvider: IContextViewProvider, private readonly _showOptionButtons: boolean, options: IFindInputOptions) {
super();
this.contextViewProvider = contextViewProvider;
this.placeholder = options.placeholder || '';
this.validation = options.validation;
this.label = options.label || NLS_DEFAULT_LABEL;
this.inputActiveOptionBorder = options.inputActiveOptionBorder;
this.inputActiveOptionForeground = options.inputActiveOptionForeground;
this.inputActiveOptionBackground = options.inputActiveOptionBackground;
this.inputBackground = options.inputBackground;
this.inputForeground = options.inputForeground;
this.inputBorder = options.inputBorder;
this.inputValidationInfoBorder = options.inputValidationInfoBorder;
this.inputValidationInfoBackground = options.inputValidationInfoBackground;
this.inputValidationInfoForeground = options.inputValidationInfoForeground;
this.inputValidationWarningBorder = options.inputValidationWarningBorder;
this.inputValidationWarningBackground = options.inputValidationWarningBackground;
this.inputValidationWarningForeground = options.inputValidationWarningForeground;
this.inputValidationErrorBorder = options.inputValidationErrorBorder;
this.inputValidationErrorBackground = options.inputValidationErrorBackground;
this.inputValidationErrorForeground = options.inputValidationErrorForeground;
const appendCaseSensitiveLabel = options.appendCaseSensitiveLabel || '';
const appendWholeWordsLabel = options.appendWholeWordsLabel || '';
const appendRegexLabel = options.appendRegexLabel || '';
const history = options.history || [];
const flexibleHeight = !!options.flexibleHeight;
const flexibleWidth = !!options.flexibleWidth;
const flexibleMaxHeight = options.flexibleMaxHeight;
this.domNode = document.createElement('div');
this.domNode.classList.add('monaco-findInput');
this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, {
placeholder: this.placeholder || '',
ariaLabel: this.label || '',
validationOptions: {
validation: this.validation
},
inputBackground: this.inputBackground,
inputForeground: this.inputForeground,
inputBorder: this.inputBorder,
inputValidationInfoBackground: this.inputValidationInfoBackground,
inputValidationInfoForeground: this.inputValidationInfoForeground,
inputValidationInfoBorder: this.inputValidationInfoBorder,
inputValidationWarningBackground: this.inputValidationWarningBackground,
inputValidationWarningForeground: this.inputValidationWarningForeground,
inputValidationWarningBorder: this.inputValidationWarningBorder,
inputValidationErrorBackground: this.inputValidationErrorBackground,
inputValidationErrorForeground: this.inputValidationErrorForeground,
inputValidationErrorBorder: this.inputValidationErrorBorder,
history,
flexibleHeight,
flexibleWidth,
flexibleMaxHeight
}));
this.regex = this._register(new RegexCheckbox({
appendTitle: appendRegexLabel,
isChecked: false,
inputActiveOptionBorder: this.inputActiveOptionBorder,
inputActiveOptionForeground: this.inputActiveOptionForeground,
inputActiveOptionBackground: this.inputActiveOptionBackground
}));
this._register(this.regex.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
this.validate();
}));
this._register(this.regex.onKeyDown(e => {
this._onRegexKeyDown.fire(e);
}));
this.wholeWords = this._register(new WholeWordsCheckbox({
appendTitle: appendWholeWordsLabel,
isChecked: false,
inputActiveOptionBorder: this.inputActiveOptionBorder,
inputActiveOptionForeground: this.inputActiveOptionForeground,
inputActiveOptionBackground: this.inputActiveOptionBackground
}));
this._register(this.wholeWords.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
this.validate();
}));
this.caseSensitive = this._register(new CaseSensitiveCheckbox({
appendTitle: appendCaseSensitiveLabel,
isChecked: false,
inputActiveOptionBorder: this.inputActiveOptionBorder,
inputActiveOptionForeground: this.inputActiveOptionForeground,
inputActiveOptionBackground: this.inputActiveOptionBackground
}));
this._register(this.caseSensitive.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
this.validate();
}));
this._register(this.caseSensitive.onKeyDown(e => {
this._onCaseSensitiveKeyDown.fire(e);
}));
if (this._showOptionButtons) {
this.inputBox.paddingRight = this.caseSensitive.width() + this.wholeWords.width() + this.regex.width();
}
// Arrow-Key support to navigate between options
let indexes = [this.caseSensitive.domNode, this.wholeWords.domNode, this.regex.domNode];
this.onkeydown(this.domNode, (event: IKeyboardEvent) => {
if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Escape)) {
let index = indexes.indexOf(<HTMLElement>document.activeElement);
if (index >= 0) {
let newIndex: number = -1;
if (event.equals(KeyCode.RightArrow)) {
newIndex = (index + 1) % indexes.length;
} else if (event.equals(KeyCode.LeftArrow)) {
if (index === 0) {
newIndex = indexes.length - 1;
} else {
newIndex = index - 1;
}
}
if (event.equals(KeyCode.Escape)) {
indexes[index].blur();
this.inputBox.focus();
} else if (newIndex >= 0) {
indexes[newIndex].focus();
}
dom.EventHelper.stop(event, true);
}
}
});
let controls = document.createElement('div');
controls.className = 'controls';
controls.style.display = this._showOptionButtons ? 'block' : 'none';
controls.appendChild(this.caseSensitive.domNode);
controls.appendChild(this.wholeWords.domNode);
controls.appendChild(this.regex.domNode);
this.domNode.appendChild(controls);
if (parent) {
parent.appendChild(this.domNode);
}
this.onkeydown(this.inputBox.inputElement, (e) => this._onKeyDown.fire(e));
this.onkeyup(this.inputBox.inputElement, (e) => this._onKeyUp.fire(e));
this.oninput(this.inputBox.inputElement, (e) => this._onInput.fire());
this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e));
}
public enable(): void {
this.domNode.classList.remove('disabled');
this.inputBox.enable();
this.regex.enable();
this.wholeWords.enable();
this.caseSensitive.enable();
}
public disable(): void {
this.domNode.classList.add('disabled');
this.inputBox.disable();
this.regex.disable();
this.wholeWords.disable();
this.caseSensitive.disable();
}
public setFocusInputOnOptionClick(value: boolean): void {
this.fixFocusOnOptionClickEnabled = value;
}
public setEnabled(enabled: boolean): void {
if (enabled) {
this.enable();
} else {
this.disable();
}
}
public clear(): void {
this.clearValidation();
this.setValue('');
this.focus();
}
public getValue(): string {
return this.inputBox.value;
}
public setValue(value: string): void {
if (this.inputBox.value !== value) {
this.inputBox.value = value;
}
}
public onSearchSubmit(): void {
this.inputBox.addToHistory();
}
public style(styles: IFindInputStyles): void {
this.inputActiveOptionBorder = styles.inputActiveOptionBorder;
this.inputActiveOptionForeground = styles.inputActiveOptionForeground;
this.inputActiveOptionBackground = styles.inputActiveOptionBackground;
this.inputBackground = styles.inputBackground;
this.inputForeground = styles.inputForeground;
this.inputBorder = styles.inputBorder;
this.inputValidationInfoBackground = styles.inputValidationInfoBackground;
this.inputValidationInfoForeground = styles.inputValidationInfoForeground;
this.inputValidationInfoBorder = styles.inputValidationInfoBorder;
this.inputValidationWarningBackground = styles.inputValidationWarningBackground;
this.inputValidationWarningForeground = styles.inputValidationWarningForeground;
this.inputValidationWarningBorder = styles.inputValidationWarningBorder;
this.inputValidationErrorBackground = styles.inputValidationErrorBackground;
this.inputValidationErrorForeground = styles.inputValidationErrorForeground;
this.inputValidationErrorBorder = styles.inputValidationErrorBorder;
this.applyStyles();
}
protected applyStyles(): void {
if (this.domNode) {
const checkBoxStyles: ICheckboxStyles = {
inputActiveOptionBorder: this.inputActiveOptionBorder,
inputActiveOptionForeground: this.inputActiveOptionForeground,
inputActiveOptionBackground: this.inputActiveOptionBackground,
};
this.regex.style(checkBoxStyles);
this.wholeWords.style(checkBoxStyles);
this.caseSensitive.style(checkBoxStyles);
const inputBoxStyles: IInputBoxStyles = {
inputBackground: this.inputBackground,
inputForeground: this.inputForeground,
inputBorder: this.inputBorder,
inputValidationInfoBackground: this.inputValidationInfoBackground,
inputValidationInfoForeground: this.inputValidationInfoForeground,
inputValidationInfoBorder: this.inputValidationInfoBorder,
inputValidationWarningBackground: this.inputValidationWarningBackground,
inputValidationWarningForeground: this.inputValidationWarningForeground,
inputValidationWarningBorder: this.inputValidationWarningBorder,
inputValidationErrorBackground: this.inputValidationErrorBackground,
inputValidationErrorForeground: this.inputValidationErrorForeground,
inputValidationErrorBorder: this.inputValidationErrorBorder
};
this.inputBox.style(inputBoxStyles);
}
}
public select(): void {
this.inputBox.select();
}
public focus(): void {
this.inputBox.focus();
}
public getCaseSensitive(): boolean {
return this.caseSensitive.checked;
}
public setCaseSensitive(value: boolean): void {
this.caseSensitive.checked = value;
}
public getWholeWords(): boolean {
return this.wholeWords.checked;
}
public setWholeWords(value: boolean): void {
this.wholeWords.checked = value;
}
public getRegex(): boolean {
return this.regex.checked;
}
public setRegex(value: boolean): void {
this.regex.checked = value;
this.validate();
}
public focusOnCaseSensitive(): void {
this.caseSensitive.focus();
}
public focusOnRegex(): void {
this.regex.focus();
}
private _lastHighlightFindOptions: number = 0;
public highlightFindOptions(): void {
this.domNode.classList.remove('highlight-' + (this._lastHighlightFindOptions));
this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions;
this.domNode.classList.add('highlight-' + (this._lastHighlightFindOptions));
}
public validate(): void {
this.inputBox.validate();
}
public showMessage(message: InputBoxMessage): void {
this.inputBox.showMessage(message);
}
public clearMessage(): void {
this.inputBox.hideMessage();
}
private clearValidation(): void {
this.inputBox.hideMessage();
}
}

View File

@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox';
import { Color } from 'vs/base/common/color';
import * as nls from 'vs/nls';
import { Codicon } from 'vs/base/common/codicons';
export interface IFindInputCheckboxOpts {
readonly appendTitle: string;
readonly isChecked: boolean;
readonly inputActiveOptionBorder?: Color;
readonly inputActiveOptionForeground?: Color;
readonly inputActiveOptionBackground?: Color;
}
const NLS_CASE_SENSITIVE_CHECKBOX_LABEL = nls.localize('caseDescription', "Match Case");
const NLS_WHOLE_WORD_CHECKBOX_LABEL = nls.localize('wordsDescription', "Match Whole Word");
const NLS_REGEX_CHECKBOX_LABEL = nls.localize('regexDescription', "Use Regular Expression");
export class CaseSensitiveCheckbox extends Checkbox {
constructor(opts: IFindInputCheckboxOpts) {
super({
icon: Codicon.caseSensitive,
title: NLS_CASE_SENSITIVE_CHECKBOX_LABEL + opts.appendTitle,
isChecked: opts.isChecked,
inputActiveOptionBorder: opts.inputActiveOptionBorder,
inputActiveOptionForeground: opts.inputActiveOptionForeground,
inputActiveOptionBackground: opts.inputActiveOptionBackground
});
}
}
export class WholeWordsCheckbox extends Checkbox {
constructor(opts: IFindInputCheckboxOpts) {
super({
icon: Codicon.wholeWord,
title: NLS_WHOLE_WORD_CHECKBOX_LABEL + opts.appendTitle,
isChecked: opts.isChecked,
inputActiveOptionBorder: opts.inputActiveOptionBorder,
inputActiveOptionForeground: opts.inputActiveOptionForeground,
inputActiveOptionBackground: opts.inputActiveOptionBackground
});
}
}
export class RegexCheckbox extends Checkbox {
constructor(opts: IFindInputCheckboxOpts) {
super({
icon: Codicon.regex,
title: NLS_REGEX_CHECKBOX_LABEL + opts.appendTitle,
isChecked: opts.isChecked,
inputActiveOptionBorder: opts.inputActiveOptionBorder,
inputActiveOptionForeground: opts.inputActiveOptionForeground,
inputActiveOptionBackground: opts.inputActiveOptionBackground
});
}
}

View File

@ -0,0 +1,388 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./findInput';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { IMessage as InputBoxMessage, IInputValidator, IInputBoxStyles, HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { Widget } from 'vs/base/browser/ui/widget';
import { Event, Emitter } from 'vs/base/common/event';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Color } from 'vs/base/common/color';
import { ICheckboxStyles, Checkbox } from 'vs/base/browser/ui/checkbox/checkbox';
import { IFindInputCheckboxOpts } from 'vs/base/browser/ui/findinput/findInputCheckboxes';
import { Codicon } from 'vs/base/common/codicons';
export interface IReplaceInputOptions extends IReplaceInputStyles {
readonly placeholder?: string;
readonly width?: number;
readonly validation?: IInputValidator;
readonly label: string;
readonly flexibleHeight?: boolean;
readonly flexibleWidth?: boolean;
readonly flexibleMaxHeight?: number;
readonly appendPreserveCaseLabel?: string;
readonly history?: string[];
}
export interface IReplaceInputStyles extends IInputBoxStyles {
inputActiveOptionBorder?: Color;
inputActiveOptionForeground?: Color;
inputActiveOptionBackground?: Color;
}
const NLS_DEFAULT_LABEL = nls.localize('defaultLabel', "input");
const NLS_PRESERVE_CASE_LABEL = nls.localize('label.preserveCaseCheckbox', "Preserve Case");
export class PreserveCaseCheckbox extends Checkbox {
constructor(opts: IFindInputCheckboxOpts) {
super({
// TODO: does this need its own icon?
icon: Codicon.preserveCase,
title: NLS_PRESERVE_CASE_LABEL + opts.appendTitle,
isChecked: opts.isChecked,
inputActiveOptionBorder: opts.inputActiveOptionBorder,
inputActiveOptionForeground: opts.inputActiveOptionForeground,
inputActiveOptionBackground: opts.inputActiveOptionBackground
});
}
}
export class ReplaceInput extends Widget {
static readonly OPTION_CHANGE: string = 'optionChange';
private contextViewProvider: IContextViewProvider | undefined;
private placeholder: string;
private validation?: IInputValidator;
private label: string;
private fixFocusOnOptionClickEnabled = true;
private inputActiveOptionBorder?: Color;
private inputActiveOptionForeground?: Color;
private inputActiveOptionBackground?: Color;
private inputBackground?: Color;
private inputForeground?: Color;
private inputBorder?: Color;
private inputValidationInfoBorder?: Color;
private inputValidationInfoBackground?: Color;
private inputValidationInfoForeground?: Color;
private inputValidationWarningBorder?: Color;
private inputValidationWarningBackground?: Color;
private inputValidationWarningForeground?: Color;
private inputValidationErrorBorder?: Color;
private inputValidationErrorBackground?: Color;
private inputValidationErrorForeground?: Color;
private preserveCase: PreserveCaseCheckbox;
private cachedOptionsWidth: number = 0;
public domNode: HTMLElement;
public inputBox: HistoryInputBox;
private readonly _onDidOptionChange = this._register(new Emitter<boolean>());
public readonly onDidOptionChange: Event<boolean /* via keyboard */> = this._onDidOptionChange.event;
private readonly _onKeyDown = this._register(new Emitter<IKeyboardEvent>());
public readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;
private readonly _onMouseDown = this._register(new Emitter<IMouseEvent>());
public readonly onMouseDown: Event<IMouseEvent> = this._onMouseDown.event;
private readonly _onInput = this._register(new Emitter<void>());
public readonly onInput: Event<void> = this._onInput.event;
private readonly _onKeyUp = this._register(new Emitter<IKeyboardEvent>());
public readonly onKeyUp: Event<IKeyboardEvent> = this._onKeyUp.event;
private _onPreserveCaseKeyDown = this._register(new Emitter<IKeyboardEvent>());
public readonly onPreserveCaseKeyDown: Event<IKeyboardEvent> = this._onPreserveCaseKeyDown.event;
constructor(parent: HTMLElement | null, contextViewProvider: IContextViewProvider | undefined, private readonly _showOptionButtons: boolean, options: IReplaceInputOptions) {
super();
this.contextViewProvider = contextViewProvider;
this.placeholder = options.placeholder || '';
this.validation = options.validation;
this.label = options.label || NLS_DEFAULT_LABEL;
this.inputActiveOptionBorder = options.inputActiveOptionBorder;
this.inputActiveOptionForeground = options.inputActiveOptionForeground;
this.inputActiveOptionBackground = options.inputActiveOptionBackground;
this.inputBackground = options.inputBackground;
this.inputForeground = options.inputForeground;
this.inputBorder = options.inputBorder;
this.inputValidationInfoBorder = options.inputValidationInfoBorder;
this.inputValidationInfoBackground = options.inputValidationInfoBackground;
this.inputValidationInfoForeground = options.inputValidationInfoForeground;
this.inputValidationWarningBorder = options.inputValidationWarningBorder;
this.inputValidationWarningBackground = options.inputValidationWarningBackground;
this.inputValidationWarningForeground = options.inputValidationWarningForeground;
this.inputValidationErrorBorder = options.inputValidationErrorBorder;
this.inputValidationErrorBackground = options.inputValidationErrorBackground;
this.inputValidationErrorForeground = options.inputValidationErrorForeground;
const appendPreserveCaseLabel = options.appendPreserveCaseLabel || '';
const history = options.history || [];
const flexibleHeight = !!options.flexibleHeight;
const flexibleWidth = !!options.flexibleWidth;
const flexibleMaxHeight = options.flexibleMaxHeight;
this.domNode = document.createElement('div');
this.domNode.classList.add('monaco-findInput');
this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, {
ariaLabel: this.label || '',
placeholder: this.placeholder || '',
validationOptions: {
validation: this.validation
},
inputBackground: this.inputBackground,
inputForeground: this.inputForeground,
inputBorder: this.inputBorder,
inputValidationInfoBackground: this.inputValidationInfoBackground,
inputValidationInfoForeground: this.inputValidationInfoForeground,
inputValidationInfoBorder: this.inputValidationInfoBorder,
inputValidationWarningBackground: this.inputValidationWarningBackground,
inputValidationWarningForeground: this.inputValidationWarningForeground,
inputValidationWarningBorder: this.inputValidationWarningBorder,
inputValidationErrorBackground: this.inputValidationErrorBackground,
inputValidationErrorForeground: this.inputValidationErrorForeground,
inputValidationErrorBorder: this.inputValidationErrorBorder,
history,
flexibleHeight,
flexibleWidth,
flexibleMaxHeight
}));
this.preserveCase = this._register(new PreserveCaseCheckbox({
appendTitle: appendPreserveCaseLabel,
isChecked: false,
inputActiveOptionBorder: this.inputActiveOptionBorder,
inputActiveOptionForeground: this.inputActiveOptionForeground,
inputActiveOptionBackground: this.inputActiveOptionBackground,
}));
this._register(this.preserveCase.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
this.validate();
}));
this._register(this.preserveCase.onKeyDown(e => {
this._onPreserveCaseKeyDown.fire(e);
}));
if (this._showOptionButtons) {
this.cachedOptionsWidth = this.preserveCase.width();
} else {
this.cachedOptionsWidth = 0;
}
// Arrow-Key support to navigate between options
let indexes = [this.preserveCase.domNode];
this.onkeydown(this.domNode, (event: IKeyboardEvent) => {
if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Escape)) {
let index = indexes.indexOf(<HTMLElement>document.activeElement);
if (index >= 0) {
let newIndex: number = -1;
if (event.equals(KeyCode.RightArrow)) {
newIndex = (index + 1) % indexes.length;
} else if (event.equals(KeyCode.LeftArrow)) {
if (index === 0) {
newIndex = indexes.length - 1;
} else {
newIndex = index - 1;
}
}
if (event.equals(KeyCode.Escape)) {
indexes[index].blur();
this.inputBox.focus();
} else if (newIndex >= 0) {
indexes[newIndex].focus();
}
dom.EventHelper.stop(event, true);
}
}
});
let controls = document.createElement('div');
controls.className = 'controls';
controls.style.display = this._showOptionButtons ? 'block' : 'none';
controls.appendChild(this.preserveCase.domNode);
this.domNode.appendChild(controls);
if (parent) {
parent.appendChild(this.domNode);
}
this.onkeydown(this.inputBox.inputElement, (e) => this._onKeyDown.fire(e));
this.onkeyup(this.inputBox.inputElement, (e) => this._onKeyUp.fire(e));
this.oninput(this.inputBox.inputElement, (e) => this._onInput.fire());
this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e));
}
public enable(): void {
this.domNode.classList.remove('disabled');
this.inputBox.enable();
this.preserveCase.enable();
}
public disable(): void {
this.domNode.classList.add('disabled');
this.inputBox.disable();
this.preserveCase.disable();
}
public setFocusInputOnOptionClick(value: boolean): void {
this.fixFocusOnOptionClickEnabled = value;
}
public setEnabled(enabled: boolean): void {
if (enabled) {
this.enable();
} else {
this.disable();
}
}
public clear(): void {
this.clearValidation();
this.setValue('');
this.focus();
}
public getValue(): string {
return this.inputBox.value;
}
public setValue(value: string): void {
if (this.inputBox.value !== value) {
this.inputBox.value = value;
}
}
public onSearchSubmit(): void {
this.inputBox.addToHistory();
}
public style(styles: IReplaceInputStyles): void {
this.inputActiveOptionBorder = styles.inputActiveOptionBorder;
this.inputActiveOptionForeground = styles.inputActiveOptionForeground;
this.inputActiveOptionBackground = styles.inputActiveOptionBackground;
this.inputBackground = styles.inputBackground;
this.inputForeground = styles.inputForeground;
this.inputBorder = styles.inputBorder;
this.inputValidationInfoBackground = styles.inputValidationInfoBackground;
this.inputValidationInfoForeground = styles.inputValidationInfoForeground;
this.inputValidationInfoBorder = styles.inputValidationInfoBorder;
this.inputValidationWarningBackground = styles.inputValidationWarningBackground;
this.inputValidationWarningForeground = styles.inputValidationWarningForeground;
this.inputValidationWarningBorder = styles.inputValidationWarningBorder;
this.inputValidationErrorBackground = styles.inputValidationErrorBackground;
this.inputValidationErrorForeground = styles.inputValidationErrorForeground;
this.inputValidationErrorBorder = styles.inputValidationErrorBorder;
this.applyStyles();
}
protected applyStyles(): void {
if (this.domNode) {
const checkBoxStyles: ICheckboxStyles = {
inputActiveOptionBorder: this.inputActiveOptionBorder,
inputActiveOptionForeground: this.inputActiveOptionForeground,
inputActiveOptionBackground: this.inputActiveOptionBackground,
};
this.preserveCase.style(checkBoxStyles);
const inputBoxStyles: IInputBoxStyles = {
inputBackground: this.inputBackground,
inputForeground: this.inputForeground,
inputBorder: this.inputBorder,
inputValidationInfoBackground: this.inputValidationInfoBackground,
inputValidationInfoForeground: this.inputValidationInfoForeground,
inputValidationInfoBorder: this.inputValidationInfoBorder,
inputValidationWarningBackground: this.inputValidationWarningBackground,
inputValidationWarningForeground: this.inputValidationWarningForeground,
inputValidationWarningBorder: this.inputValidationWarningBorder,
inputValidationErrorBackground: this.inputValidationErrorBackground,
inputValidationErrorForeground: this.inputValidationErrorForeground,
inputValidationErrorBorder: this.inputValidationErrorBorder
};
this.inputBox.style(inputBoxStyles);
}
}
public select(): void {
this.inputBox.select();
}
public focus(): void {
this.inputBox.focus();
}
public getPreserveCase(): boolean {
return this.preserveCase.checked;
}
public setPreserveCase(value: boolean): void {
this.preserveCase.checked = value;
}
public focusOnPreserve(): void {
this.preserveCase.focus();
}
private _lastHighlightFindOptions: number = 0;
public highlightFindOptions(): void {
this.domNode.classList.remove('highlight-' + (this._lastHighlightFindOptions));
this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions;
this.domNode.classList.add('highlight-' + (this._lastHighlightFindOptions));
}
public validate(): void {
if (this.inputBox) {
this.inputBox.validate();
}
}
public showMessage(message: InputBoxMessage): void {
if (this.inputBox) {
this.inputBox.showMessage(message);
}
}
public clearMessage(): void {
if (this.inputBox) {
this.inputBox.hideMessage();
}
}
private clearValidation(): void {
if (this.inputBox) {
this.inputBox.hideMessage();
}
}
public set width(newWidth: number) {
this.inputBox.paddingRight = this.cachedOptionsWidth;
this.inputBox.width = newWidth;
this.domNode.style.width = newWidth + 'px';
}
public dispose(): void {
super.dispose();
}
}

View File

@ -0,0 +1,682 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./gridview';
import { Orientation } from 'vs/base/browser/ui/sash/sash';
import { Disposable } from 'vs/base/common/lifecycle';
import { tail2 as tail, equals } from 'vs/base/common/arrays';
import { orthogonal, IView as IGridViewView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles, IViewSize, IGridViewOptions, IBoundarySashes } from './gridview';
import { Event } from 'vs/base/common/event';
export { Orientation, IViewSize, orthogonal, LayoutPriority } from './gridview';
export const enum Direction {
Up,
Down,
Left,
Right
}
function oppositeDirection(direction: Direction): Direction {
switch (direction) {
case Direction.Up: return Direction.Down;
case Direction.Down: return Direction.Up;
case Direction.Left: return Direction.Right;
case Direction.Right: return Direction.Left;
}
}
export interface IView extends IGridViewView {
readonly preferredHeight?: number;
readonly preferredWidth?: number;
}
export interface GridLeafNode<T extends IView> {
readonly view: T;
readonly box: Box;
readonly cachedVisibleSize: number | undefined;
}
export interface GridBranchNode<T extends IView> {
readonly children: GridNode<T>[];
readonly box: Box;
}
export type GridNode<T extends IView> = GridLeafNode<T> | GridBranchNode<T>;
export function isGridBranchNode<T extends IView>(node: GridNode<T>): node is GridBranchNode<T> {
return !!(node as any).children;
}
function getGridNode<T extends IView>(node: GridNode<T>, location: number[]): GridNode<T> {
if (location.length === 0) {
return node;
}
if (!isGridBranchNode(node)) {
throw new Error('Invalid location');
}
const [index, ...rest] = location;
return getGridNode(node.children[index], rest);
}
interface Range {
readonly start: number;
readonly end: number;
}
function intersects(one: Range, other: Range): boolean {
return !(one.start >= other.end || other.start >= one.end);
}
interface Boundary {
readonly offset: number;
readonly range: Range;
}
function getBoxBoundary(box: Box, direction: Direction): Boundary {
const orientation = getDirectionOrientation(direction);
const offset = direction === Direction.Up ? box.top :
direction === Direction.Right ? box.left + box.width :
direction === Direction.Down ? box.top + box.height :
box.left;
const range = {
start: orientation === Orientation.HORIZONTAL ? box.top : box.left,
end: orientation === Orientation.HORIZONTAL ? box.top + box.height : box.left + box.width
};
return { offset, range };
}
function findAdjacentBoxLeafNodes<T extends IView>(boxNode: GridNode<T>, direction: Direction, boundary: Boundary): GridLeafNode<T>[] {
const result: GridLeafNode<T>[] = [];
function _(boxNode: GridNode<T>, direction: Direction, boundary: Boundary): void {
if (isGridBranchNode(boxNode)) {
for (const child of boxNode.children) {
_(child, direction, boundary);
}
} else {
const { offset, range } = getBoxBoundary(boxNode.box, direction);
if (offset === boundary.offset && intersects(range, boundary.range)) {
result.push(boxNode);
}
}
}
_(boxNode, direction, boundary);
return result;
}
function getLocationOrientation(rootOrientation: Orientation, location: number[]): Orientation {
return location.length % 2 === 0 ? orthogonal(rootOrientation) : rootOrientation;
}
function getDirectionOrientation(direction: Direction): Orientation {
return direction === Direction.Up || direction === Direction.Down ? Orientation.VERTICAL : Orientation.HORIZONTAL;
}
export function getRelativeLocation(rootOrientation: Orientation, location: number[], direction: Direction): number[] {
const orientation = getLocationOrientation(rootOrientation, location);
const directionOrientation = getDirectionOrientation(direction);
if (orientation === directionOrientation) {
let [rest, index] = tail(location);
if (direction === Direction.Right || direction === Direction.Down) {
index += 1;
}
return [...rest, index];
} else {
const index = (direction === Direction.Right || direction === Direction.Down) ? 1 : 0;
return [...location, index];
}
}
function indexInParent(element: HTMLElement): number {
const parentElement = element.parentElement;
if (!parentElement) {
throw new Error('Invalid grid element');
}
let el = parentElement.firstElementChild;
let index = 0;
while (el !== element && el !== parentElement.lastElementChild && el) {
el = el.nextElementSibling;
index++;
}
return index;
}
/**
* Find the grid location of a specific DOM element by traversing the parent
* chain and finding each child index on the way.
*
* This will break as soon as DOM structures of the Splitview or Gridview change.
*/
function getGridLocation(element: HTMLElement): number[] {
const parentElement = element.parentElement;
if (!parentElement) {
throw new Error('Invalid grid element');
}
if (/\bmonaco-grid-view\b/.test(parentElement.className)) {
return [];
}
const index = indexInParent(parentElement);
const ancestor = parentElement.parentElement!.parentElement!.parentElement!;
return [...getGridLocation(ancestor), index];
}
export type DistributeSizing = { type: 'distribute' };
export type SplitSizing = { type: 'split' };
export type InvisibleSizing = { type: 'invisible', cachedVisibleSize: number };
export type Sizing = DistributeSizing | SplitSizing | InvisibleSizing;
export namespace Sizing {
export const Distribute: DistributeSizing = { type: 'distribute' };
export const Split: SplitSizing = { type: 'split' };
export function Invisible(cachedVisibleSize: number): InvisibleSizing { return { type: 'invisible', cachedVisibleSize }; }
}
export interface IGridStyles extends IGridViewStyles { }
export interface IGridOptions extends IGridViewOptions {
readonly firstViewVisibleCachedSize?: number;
}
export class Grid<T extends IView = IView> extends Disposable {
protected gridview: GridView;
private views = new Map<T, HTMLElement>();
get orientation(): Orientation { return this.gridview.orientation; }
set orientation(orientation: Orientation) { this.gridview.orientation = orientation; }
get width(): number { return this.gridview.width; }
get height(): number { return this.gridview.height; }
get minimumWidth(): number { return this.gridview.minimumWidth; }
get minimumHeight(): number { return this.gridview.minimumHeight; }
get maximumWidth(): number { return this.gridview.maximumWidth; }
get maximumHeight(): number { return this.gridview.maximumHeight; }
get onDidChange(): Event<{ width: number; height: number; } | undefined> { return this.gridview.onDidChange; }
get boundarySashes(): IBoundarySashes { return this.gridview.boundarySashes; }
set boundarySashes(boundarySashes: IBoundarySashes) { this.gridview.boundarySashes = boundarySashes; }
get element(): HTMLElement { return this.gridview.element; }
private didLayout = false;
constructor(gridview: GridView, options?: IGridOptions);
constructor(view: T, options?: IGridOptions);
constructor(view: T | GridView, options: IGridOptions = {}) {
super();
if (view instanceof GridView) {
this.gridview = view;
this.gridview.getViewMap(this.views);
} else {
this.gridview = new GridView(options);
}
this._register(this.gridview);
this._register(this.gridview.onDidSashReset(this.onDidSashReset, this));
const size: number | GridViewSizing = typeof options.firstViewVisibleCachedSize === 'number'
? GridViewSizing.Invisible(options.firstViewVisibleCachedSize)
: 0;
if (!(view instanceof GridView)) {
this._addView(view, size, [0]);
}
}
style(styles: IGridStyles): void {
this.gridview.style(styles);
}
layout(width: number, height: number): void {
this.gridview.layout(width, height);
this.didLayout = true;
}
hasView(view: T): boolean {
return this.views.has(view);
}
addView(newView: T, size: number | Sizing, referenceView: T, direction: Direction): void {
if (this.views.has(newView)) {
throw new Error('Can\'t add same view twice');
}
const orientation = getDirectionOrientation(direction);
if (this.views.size === 1 && this.orientation !== orientation) {
this.orientation = orientation;
}
const referenceLocation = this.getViewLocation(referenceView);
const location = getRelativeLocation(this.gridview.orientation, referenceLocation, direction);
let viewSize: number | GridViewSizing;
if (typeof size === 'number') {
viewSize = size;
} else if (size.type === 'split') {
const [, index] = tail(referenceLocation);
viewSize = GridViewSizing.Split(index);
} else if (size.type === 'distribute') {
viewSize = GridViewSizing.Distribute;
} else {
viewSize = size;
}
this._addView(newView, viewSize, location);
}
addViewAt(newView: T, size: number | DistributeSizing | InvisibleSizing, location: number[]): void {
if (this.views.has(newView)) {
throw new Error('Can\'t add same view twice');
}
let viewSize: number | GridViewSizing;
if (typeof size === 'number') {
viewSize = size;
} else if (size.type === 'distribute') {
viewSize = GridViewSizing.Distribute;
} else {
viewSize = size;
}
this._addView(newView, viewSize, location);
}
protected _addView(newView: T, size: number | GridViewSizing, location: number[]): void {
this.views.set(newView, newView.element);
this.gridview.addView(newView, size, location);
}
removeView(view: T, sizing?: Sizing): void {
if (this.views.size === 1) {
throw new Error('Can\'t remove last view');
}
const location = this.getViewLocation(view);
this.gridview.removeView(location, (sizing && sizing.type === 'distribute') ? GridViewSizing.Distribute : undefined);
this.views.delete(view);
}
moveView(view: T, sizing: number | Sizing, referenceView: T, direction: Direction): void {
const sourceLocation = this.getViewLocation(view);
const [sourceParentLocation, from] = tail(sourceLocation);
const referenceLocation = this.getViewLocation(referenceView);
const targetLocation = getRelativeLocation(this.gridview.orientation, referenceLocation, direction);
const [targetParentLocation, to] = tail(targetLocation);
if (equals(sourceParentLocation, targetParentLocation)) {
this.gridview.moveView(sourceParentLocation, from, to);
} else {
this.removeView(view, typeof sizing === 'number' ? undefined : sizing);
this.addView(view, sizing, referenceView, direction);
}
}
moveViewTo(view: T, location: number[]): void {
const sourceLocation = this.getViewLocation(view);
const [sourceParentLocation, from] = tail(sourceLocation);
const [targetParentLocation, to] = tail(location);
if (equals(sourceParentLocation, targetParentLocation)) {
this.gridview.moveView(sourceParentLocation, from, to);
} else {
const size = this.getViewSize(view);
const orientation = getLocationOrientation(this.gridview.orientation, sourceLocation);
const cachedViewSize = this.getViewCachedVisibleSize(view);
const sizing = typeof cachedViewSize === 'undefined'
? (orientation === Orientation.HORIZONTAL ? size.width : size.height)
: Sizing.Invisible(cachedViewSize);
this.removeView(view);
this.addViewAt(view, sizing, location);
}
}
swapViews(from: T, to: T): void {
const fromLocation = this.getViewLocation(from);
const toLocation = this.getViewLocation(to);
return this.gridview.swapViews(fromLocation, toLocation);
}
resizeView(view: T, size: IViewSize): void {
const location = this.getViewLocation(view);
return this.gridview.resizeView(location, size);
}
getViewSize(view?: T): IViewSize {
if (!view) {
return this.gridview.getViewSize();
}
const location = this.getViewLocation(view);
return this.gridview.getViewSize(location);
}
getViewCachedVisibleSize(view: T): number | undefined {
const location = this.getViewLocation(view);
return this.gridview.getViewCachedVisibleSize(location);
}
maximizeViewSize(view: T): void {
const location = this.getViewLocation(view);
this.gridview.maximizeViewSize(location);
}
distributeViewSizes(): void {
this.gridview.distributeViewSizes();
}
isViewVisible(view: T): boolean {
const location = this.getViewLocation(view);
return this.gridview.isViewVisible(location);
}
setViewVisible(view: T, visible: boolean): void {
const location = this.getViewLocation(view);
this.gridview.setViewVisible(location, visible);
}
getViews(): GridBranchNode<T> {
return this.gridview.getView() as GridBranchNode<T>;
}
getNeighborViews(view: T, direction: Direction, wrap: boolean = false): T[] {
if (!this.didLayout) {
throw new Error('Can\'t call getNeighborViews before first layout');
}
const location = this.getViewLocation(view);
const root = this.getViews();
const node = getGridNode(root, location);
let boundary = getBoxBoundary(node.box, direction);
if (wrap) {
if (direction === Direction.Up && node.box.top === 0) {
boundary = { offset: root.box.top + root.box.height, range: boundary.range };
} else if (direction === Direction.Right && node.box.left + node.box.width === root.box.width) {
boundary = { offset: 0, range: boundary.range };
} else if (direction === Direction.Down && node.box.top + node.box.height === root.box.height) {
boundary = { offset: 0, range: boundary.range };
} else if (direction === Direction.Left && node.box.left === 0) {
boundary = { offset: root.box.left + root.box.width, range: boundary.range };
}
}
return findAdjacentBoxLeafNodes(root, oppositeDirection(direction), boundary)
.map(node => node.view);
}
getViewLocation(view: T): number[] {
const element = this.views.get(view);
if (!element) {
throw new Error('View not found');
}
return getGridLocation(element);
}
private onDidSashReset(location: number[]): void {
const resizeToPreferredSize = (location: number[]): boolean => {
const node = this.gridview.getView(location) as GridNode<T>;
if (isGridBranchNode(node)) {
return false;
}
const direction = getLocationOrientation(this.orientation, location);
const size = direction === Orientation.HORIZONTAL ? node.view.preferredWidth : node.view.preferredHeight;
if (typeof size !== 'number') {
return false;
}
const viewSize = direction === Orientation.HORIZONTAL ? { width: Math.round(size) } : { height: Math.round(size) };
this.gridview.resizeView(location, viewSize);
return true;
};
if (resizeToPreferredSize(location)) {
return;
}
const [parentLocation, index] = tail(location);
if (resizeToPreferredSize([...parentLocation, index + 1])) {
return;
}
this.gridview.distributeViewSizes(parentLocation);
}
}
export interface ISerializableView extends IView {
toJSON(): object;
}
export interface IViewDeserializer<T extends ISerializableView> {
fromJSON(json: any): T;
}
export interface ISerializedLeafNode {
type: 'leaf';
data: any;
size: number;
visible?: boolean;
}
export interface ISerializedBranchNode {
type: 'branch';
data: ISerializedNode[];
size: number;
}
export type ISerializedNode = ISerializedLeafNode | ISerializedBranchNode;
export interface ISerializedGrid {
root: ISerializedNode;
orientation: Orientation;
width: number;
height: number;
}
export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
private static serializeNode<T extends ISerializableView>(node: GridNode<T>, orientation: Orientation): ISerializedNode {
const size = orientation === Orientation.VERTICAL ? node.box.width : node.box.height;
if (!isGridBranchNode(node)) {
if (typeof node.cachedVisibleSize === 'number') {
return { type: 'leaf', data: node.view.toJSON(), size: node.cachedVisibleSize, visible: false };
}
return { type: 'leaf', data: node.view.toJSON(), size };
}
return { type: 'branch', data: node.children.map(c => SerializableGrid.serializeNode(c, orthogonal(orientation))), size };
}
private static deserializeNode<T extends ISerializableView>(json: ISerializedNode, orientation: Orientation, box: Box, deserializer: IViewDeserializer<T>): GridNode<T> {
if (!json || typeof json !== 'object') {
throw new Error('Invalid JSON');
}
if (json.type === 'branch') {
if (!Array.isArray(json.data)) {
throw new Error('Invalid JSON: \'data\' property of branch must be an array.');
}
const children: GridNode<T>[] = [];
let offset = 0;
for (const child of json.data) {
if (typeof child.size !== 'number') {
throw new Error('Invalid JSON: \'size\' property of node must be a number.');
}
const childSize = child.type === 'leaf' && child.visible === false ? 0 : child.size;
const childBox: Box = orientation === Orientation.HORIZONTAL
? { top: box.top, left: box.left + offset, width: childSize, height: box.height }
: { top: box.top + offset, left: box.left, width: box.width, height: childSize };
children.push(SerializableGrid.deserializeNode(child, orthogonal(orientation), childBox, deserializer));
offset += childSize;
}
return { children, box };
} else if (json.type === 'leaf') {
const view: T = deserializer.fromJSON(json.data);
return { view, box, cachedVisibleSize: json.visible === false ? json.size : undefined };
}
throw new Error('Invalid JSON: \'type\' property must be either \'branch\' or \'leaf\'.');
}
private static getFirstLeaf<T extends IView>(node: GridNode<T>): GridLeafNode<T> {
if (!isGridBranchNode(node)) {
return node;
}
return SerializableGrid.getFirstLeaf(node.children[0]);
}
static deserialize<T extends ISerializableView>(json: ISerializedGrid, deserializer: IViewDeserializer<T>, options: IGridOptions = {}): SerializableGrid<T> {
if (typeof json.orientation !== 'number') {
throw new Error('Invalid JSON: \'orientation\' property must be a number.');
} else if (typeof json.width !== 'number') {
throw new Error('Invalid JSON: \'width\' property must be a number.');
} else if (typeof json.height !== 'number') {
throw new Error('Invalid JSON: \'height\' property must be a number.');
}
const gridview = GridView.deserialize(json, deserializer, options);
const result = new SerializableGrid<T>(gridview, options);
return result;
}
/**
* Useful information in order to proportionally restore view sizes
* upon the very first layout call.
*/
private initialLayoutContext: boolean = true;
serialize(): ISerializedGrid {
return {
root: SerializableGrid.serializeNode(this.getViews(), this.orientation),
orientation: this.orientation,
width: this.width,
height: this.height
};
}
layout(width: number, height: number): void {
super.layout(width, height);
if (this.initialLayoutContext) {
this.initialLayoutContext = false;
this.gridview.trySet2x2();
}
}
}
export type GridNodeDescriptor = { size?: number, groups?: GridNodeDescriptor[] };
export type GridDescriptor = { orientation: Orientation, groups?: GridNodeDescriptor[] };
export function sanitizeGridNodeDescriptor(nodeDescriptor: GridNodeDescriptor, rootNode: boolean): void {
if (!rootNode && nodeDescriptor.groups && nodeDescriptor.groups.length <= 1) {
nodeDescriptor.groups = undefined;
}
if (!nodeDescriptor.groups) {
return;
}
let totalDefinedSize = 0;
let totalDefinedSizeCount = 0;
for (const child of nodeDescriptor.groups) {
sanitizeGridNodeDescriptor(child, false);
if (child.size) {
totalDefinedSize += child.size;
totalDefinedSizeCount++;
}
}
const totalUndefinedSize = totalDefinedSizeCount > 0 ? totalDefinedSize : 1;
const totalUndefinedSizeCount = nodeDescriptor.groups.length - totalDefinedSizeCount;
const eachUndefinedSize = totalUndefinedSize / totalUndefinedSizeCount;
for (const child of nodeDescriptor.groups) {
if (!child.size) {
child.size = eachUndefinedSize;
}
}
}
function createSerializedNode(nodeDescriptor: GridNodeDescriptor): ISerializedNode {
if (nodeDescriptor.groups) {
return { type: 'branch', data: nodeDescriptor.groups.map(c => createSerializedNode(c)), size: nodeDescriptor.size! };
} else {
return { type: 'leaf', data: null, size: nodeDescriptor.size! };
}
}
function getDimensions(node: ISerializedNode, orientation: Orientation): { width?: number, height?: number } {
if (node.type === 'branch') {
const childrenDimensions = node.data.map(c => getDimensions(c, orthogonal(orientation)));
if (orientation === Orientation.VERTICAL) {
const width = node.size || (childrenDimensions.length === 0 ? undefined : Math.max(...childrenDimensions.map(d => d.width || 0)));
const height = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + (d.height || 0), 0);
return { width, height };
} else {
const width = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + (d.width || 0), 0);
const height = node.size || (childrenDimensions.length === 0 ? undefined : Math.max(...childrenDimensions.map(d => d.height || 0)));
return { width, height };
}
} else {
const width = orientation === Orientation.VERTICAL ? node.size : undefined;
const height = orientation === Orientation.VERTICAL ? undefined : node.size;
return { width, height };
}
}
export function createSerializedGrid(gridDescriptor: GridDescriptor): ISerializedGrid {
sanitizeGridNodeDescriptor(gridDescriptor, true);
const root = createSerializedNode(gridDescriptor);
const { width, height } = getDimensions(root, gridDescriptor.orientation);
return {
root,
orientation: gridDescriptor.orientation,
width: width || 1,
height: height || 1
};
}

View File

@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-grid-view {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
}
.monaco-grid-branch-node {
width: 100%;
height: 100%;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,116 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as objects from 'vs/base/common/objects';
import * as dom from 'vs/base/browser/dom';
import { renderCodicons } from 'vs/base/browser/codicons';
export interface IHighlight {
start: number;
end: number;
extraClasses?: string;
}
export class HighlightedLabel {
private readonly domNode: HTMLElement;
private text: string = '';
private title: string = '';
private highlights: IHighlight[] = [];
private didEverRender: boolean = false;
constructor(container: HTMLElement, private supportCodicons: boolean) {
this.domNode = document.createElement('span');
this.domNode.className = 'monaco-highlighted-label';
container.appendChild(this.domNode);
}
get element(): HTMLElement {
return this.domNode;
}
set(text: string | undefined, highlights: IHighlight[] = [], title: string = '', escapeNewLines?: boolean) {
if (!text) {
text = '';
}
if (escapeNewLines) {
// adjusts highlights inplace
text = HighlightedLabel.escapeNewLines(text, highlights);
}
if (this.didEverRender && this.text === text && this.title === title && objects.equals(this.highlights, highlights)) {
return;
}
this.text = text;
this.title = title;
this.highlights = highlights;
this.render();
}
private render(): void {
const children: HTMLSpanElement[] = [];
let pos = 0;
for (const highlight of this.highlights) {
if (highlight.end === highlight.start) {
continue;
}
if (pos < highlight.start) {
const substring = this.text.substring(pos, highlight.start);
children.push(dom.$('span', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]));
pos = highlight.end;
}
const substring = this.text.substring(highlight.start, highlight.end);
const element = dom.$('span.highlight', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]);
if (highlight.extraClasses) {
element.classList.add(highlight.extraClasses);
}
children.push(element);
pos = highlight.end;
}
if (pos < this.text.length) {
const substring = this.text.substring(pos,);
children.push(dom.$('span', undefined, ...this.supportCodicons ? renderCodicons(substring) : [substring]));
}
dom.reset(this.domNode, ...children);
if (this.title) {
this.domNode.title = this.title;
} else {
this.domNode.removeAttribute('title');
}
this.didEverRender = true;
}
static escapeNewLines(text: string, highlights: IHighlight[]): string {
let total = 0;
let extra = 0;
return text.replace(/\r\n|\r|\n/g, (match, offset) => {
extra = match === '\r\n' ? -1 : 0;
offset += total;
for (const highlight of highlights) {
if (highlight.end <= offset) {
continue;
}
if (highlight.start >= offset) {
highlight.start += extra;
}
if (highlight.end >= offset) {
highlight.end += extra;
}
}
total += extra;
return '\u23CE';
});
}
}

View File

@ -0,0 +1,139 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-hover {
cursor: default;
position: absolute;
overflow: hidden;
z-index: 50;
user-select: text;
-webkit-user-select: text;
-ms-user-select: text;
box-sizing: initial;
animation: fadein 100ms linear;
line-height: 1.5em;
}
.monaco-hover.hidden {
display: none;
}
.monaco-hover .hover-contents {
padding: 4px 8px;
}
.monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) {
max-width: 500px;
word-wrap: break-word;
}
.monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) hr {
/* This is a strange rule but it avoids https://github.com/microsoft/vscode/issues/96795, just 100vw on its own caused the actual hover width to increase */
min-width: calc(100% + 100vw);
}
.monaco-hover p,
.monaco-hover .code,
.monaco-hover ul {
margin: 8px 0;
}
.monaco-hover code {
font-family: var(--monaco-monospace-font);
}
.monaco-hover hr {
margin-top: 4px;
margin-bottom: -4px;
margin-left: -10px;
margin-right: -10px;
height: 1px;
}
.monaco-hover p:first-child,
.monaco-hover .code:first-child,
.monaco-hover ul:first-child {
margin-top: 0;
}
.monaco-hover p:last-child,
.monaco-hover .code:last-child,
.monaco-hover ul:last-child {
margin-bottom: 0;
}
/* MarkupContent Layout */
.monaco-hover ul {
padding-left: 20px;
}
.monaco-hover ol {
padding-left: 20px;
}
.monaco-hover li > p {
margin-bottom: 0;
}
.monaco-hover li > ul {
margin-top: 0;
}
.monaco-hover code {
border-radius: 3px;
padding: 0 0.4em;
}
.monaco-hover .monaco-tokenized-source {
white-space: pre-wrap;
word-break: break-all;
}
.monaco-hover .hover-row.status-bar {
font-size: 12px;
line-height: 22px;
}
.monaco-hover .hover-row.status-bar .actions {
display: flex;
padding: 0px 8px;
}
.monaco-hover .hover-row.status-bar .actions .action-container {
margin-right: 16px;
cursor: pointer;
}
.monaco-hover .hover-row.status-bar .actions .action-container .action .icon {
padding-right: 4px;
}
.monaco-hover .markdown-hover .hover-contents .codicon {
color: inherit;
font-size: inherit;
vertical-align: middle;
}
.monaco-hover .hover-contents a.code-link:before {
content: '(';
}
.monaco-hover .hover-contents a.code-link:after {
content: ')';
}
.monaco-hover .hover-contents a.code-link {
color: inherit;
}
.monaco-hover .hover-contents a.code-link > span {
text-decoration: underline;
/** Hack to force underline to show **/
border-bottom: 1px solid transparent;
text-underline-position: under;
}
/** Spans in markdown hovers need a margin-bottom to avoid looking cramped: https://github.com/microsoft/vscode/issues/101496 **/
.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents) span {
margin-bottom: 4px;
display: inline-block;
}

View File

@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./hover';
import * as dom from 'vs/base/browser/dom';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
const $ = dom.$;
export class HoverWidget extends Disposable {
public readonly containerDomNode: HTMLElement;
public readonly contentsDomNode: HTMLElement;
private readonly _scrollbar: DomScrollableElement;
constructor() {
super();
this.containerDomNode = document.createElement('div');
this.containerDomNode.className = 'monaco-hover';
this.containerDomNode.tabIndex = 0;
this.containerDomNode.setAttribute('role', 'tooltip');
this.contentsDomNode = document.createElement('div');
this.contentsDomNode.className = 'monaco-hover-content';
this._scrollbar = this._register(new DomScrollableElement(this.contentsDomNode, {}));
this.containerDomNode.appendChild(this._scrollbar.getDomNode());
}
public onContentsChanged(): void {
this._scrollbar.scanDomNode();
}
}
export function renderHoverAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }, keybindingLabel: string | null): IDisposable {
const actionContainer = dom.append(parent, $('div.action-container'));
const action = dom.append(actionContainer, $('a.action'));
action.setAttribute('href', '#');
action.setAttribute('role', 'button');
if (actionOptions.iconClass) {
dom.append(action, $(`span.icon.${actionOptions.iconClass}`));
}
const label = dom.append(action, $('span'));
label.textContent = keybindingLabel ? `${actionOptions.label} (${keybindingLabel})` : actionOptions.label;
return dom.addDisposableListener(actionContainer, dom.EventType.CLICK, e => {
e.stopPropagation();
e.preventDefault();
actionOptions.run(actionContainer);
});
}

View File

@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IDisposable } from 'vs/base/common/lifecycle';
export interface IHoverDelegateTarget extends IDisposable {
readonly targetElements: readonly HTMLElement[];
x?: number;
}
export interface IHoverDelegateOptions {
text: IMarkdownString | string;
target: IHoverDelegateTarget | HTMLElement;
anchorPosition?: AnchorPosition;
}
export interface IHoverDelegate {
showHover(options: IHoverDelegateOptions): IDisposable | undefined;
}

View File

@ -0,0 +1,350 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./iconlabel';
import * as dom from 'vs/base/browser/dom';
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { IMatch } from 'vs/base/common/filters';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Range } from 'vs/base/common/range';
import { equals } from 'vs/base/common/objects';
import { isMacintosh } from 'vs/base/common/platform';
import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { isString } from 'vs/base/common/types';
import { domEvent } from 'vs/base/browser/event';
export interface IIconLabelCreationOptions {
supportHighlights?: boolean;
supportDescriptionHighlights?: boolean;
supportCodicons?: boolean;
hoverDelegate?: IHoverDelegate;
}
export interface IIconLabelValueOptions {
title?: string | IMarkdownString | Promise<IMarkdownString | string | undefined>;
descriptionTitle?: string;
hideIcon?: boolean;
extraClasses?: string[];
italic?: boolean;
strikethrough?: boolean;
matches?: IMatch[];
labelEscapeNewLines?: boolean;
descriptionMatches?: IMatch[];
readonly separator?: string;
readonly domId?: string;
}
class FastLabelNode {
private disposed: boolean | undefined;
private _textContent: string | undefined;
private _className: string | undefined;
private _empty: boolean | undefined;
constructor(private _element: HTMLElement) {
}
get element(): HTMLElement {
return this._element;
}
set textContent(content: string) {
if (this.disposed || content === this._textContent) {
return;
}
this._textContent = content;
this._element.textContent = content;
}
set className(className: string) {
if (this.disposed || className === this._className) {
return;
}
this._className = className;
this._element.className = className;
}
set empty(empty: boolean) {
if (this.disposed || empty === this._empty) {
return;
}
this._empty = empty;
this._element.style.marginLeft = empty ? '0' : '';
}
dispose(): void {
this.disposed = true;
}
}
export class IconLabel extends Disposable {
private domNode: FastLabelNode;
private nameNode: Label | LabelWithHighlights;
private descriptionContainer: FastLabelNode;
private descriptionNode: FastLabelNode | HighlightedLabel | undefined;
private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel;
private hoverDelegate: IHoverDelegate | undefined = undefined;
private readonly customHovers: Map<HTMLElement, IDisposable> = new Map();
constructor(container: HTMLElement, options?: IIconLabelCreationOptions) {
super();
this.domNode = this._register(new FastLabelNode(dom.append(container, dom.$('.monaco-icon-label'))));
const labelContainer = dom.append(this.domNode.element, dom.$('.monaco-icon-label-container'));
const nameContainer = dom.append(labelContainer, dom.$('span.monaco-icon-name-container'));
this.descriptionContainer = this._register(new FastLabelNode(dom.append(labelContainer, dom.$('span.monaco-icon-description-container'))));
if (options?.supportHighlights) {
this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportCodicons);
} else {
this.nameNode = new Label(nameContainer);
}
if (options?.supportDescriptionHighlights) {
this.descriptionNodeFactory = () => new HighlightedLabel(dom.append(this.descriptionContainer.element, dom.$('span.label-description')), !!options.supportCodicons);
} else {
this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description'))));
}
if (options?.hoverDelegate) {
this.hoverDelegate = options.hoverDelegate;
}
}
get element(): HTMLElement {
return this.domNode.element;
}
setLabel(label: string | string[], description?: string, options?: IIconLabelValueOptions): void {
const classes = ['monaco-icon-label'];
if (options) {
if (options.extraClasses) {
classes.push(...options.extraClasses);
}
if (options.italic) {
classes.push('italic');
}
if (options.strikethrough) {
classes.push('strikethrough');
}
}
this.domNode.className = classes.join(' ');
this.setupHover(this.domNode.element, options?.title);
this.nameNode.setLabel(label, options);
if (description || this.descriptionNode) {
if (!this.descriptionNode) {
this.descriptionNode = this.descriptionNodeFactory(); // description node is created lazily on demand
}
if (this.descriptionNode instanceof HighlightedLabel) {
this.descriptionNode.set(description || '', options ? options.descriptionMatches : undefined);
this.setupHover(this.descriptionNode.element, options?.descriptionTitle);
} else {
this.descriptionNode.textContent = description || '';
this.setupHover(this.descriptionNode.element, options?.descriptionTitle || '');
this.descriptionNode.empty = !description;
}
}
}
private setupHover(htmlElement: HTMLElement, tooltip: string | IMarkdownString | Promise<IMarkdownString | string | undefined> | undefined): void {
const previousCustomHover = this.customHovers.get(htmlElement);
if (previousCustomHover) {
previousCustomHover.dispose();
this.customHovers.delete(htmlElement);
}
if (!tooltip) {
htmlElement.removeAttribute('title');
return;
}
if (!this.hoverDelegate) {
return this.setupNativeHover(htmlElement, tooltip);
} else {
return this.setupCustomHover(this.hoverDelegate, htmlElement, tooltip);
}
}
private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, tooltip: string | IMarkdownString | Promise<IMarkdownString | string | undefined> | undefined): void {
htmlElement.removeAttribute('title');
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
// On Mac, the delay is 1500.
const hoverDelay = isMacintosh ? 1500 : 500;
let hoverOptions: IHoverDelegateOptions | undefined;
let mouseX: number | undefined;
function mouseOver(this: HTMLElement, e: MouseEvent): any {
let isHovering = true;
function mouseMove(this: HTMLElement, e: MouseEvent): any {
mouseX = e.x;
}
function mouseLeave(this: HTMLElement, e: MouseEvent): any {
isHovering = false;
}
const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeave.bind(htmlElement));
const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement));
setTimeout(async () => {
if (isHovering && tooltip) {
// Re-use the already computed hover options if they exist.
if (!hoverOptions) {
const target: IHoverDelegateTarget = {
targetElements: [this],
dispose: () => { }
};
const resolvedTooltip = await tooltip;
if (resolvedTooltip) {
hoverOptions = {
text: resolvedTooltip,
target,
anchorPosition: AnchorPosition.BELOW
};
}
}
if (hoverOptions) {
if (mouseX !== undefined) {
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
}
hoverDelegate.showHover(hoverOptions);
}
}
mouseMoveDisposable.dispose();
mouseLeaveDisposable.dispose();
}, hoverDelay);
}
const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement)));
this.customHovers.set(htmlElement, mouseOverDisposable);
}
private setupNativeHover(htmlElement: HTMLElement, tooltip: string | IMarkdownString | Promise<IMarkdownString | string | undefined> | undefined): void {
htmlElement.title = isString(tooltip) ? tooltip : '';
}
}
class Label {
private label: string | string[] | undefined = undefined;
private singleLabel: HTMLElement | undefined = undefined;
private options: IIconLabelValueOptions | undefined;
constructor(private container: HTMLElement) { }
setLabel(label: string | string[], options?: IIconLabelValueOptions): void {
if (this.label === label && equals(this.options, options)) {
return;
}
this.label = label;
this.options = options;
if (typeof label === 'string') {
if (!this.singleLabel) {
this.container.innerText = '';
this.container.classList.remove('multiple');
this.singleLabel = dom.append(this.container, dom.$('a.label-name', { id: options?.domId }));
}
this.singleLabel.textContent = label;
} else {
this.container.innerText = '';
this.container.classList.add('multiple');
this.singleLabel = undefined;
for (let i = 0; i < label.length; i++) {
const l = label[i];
const id = options?.domId && `${options?.domId}_${i}`;
dom.append(this.container, dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' }, l));
if (i < label.length - 1) {
dom.append(this.container, dom.$('span.label-separator', undefined, options?.separator || '/'));
}
}
}
}
}
function splitMatches(labels: string[], separator: string, matches: IMatch[] | undefined): IMatch[][] | undefined {
if (!matches) {
return undefined;
}
let labelStart = 0;
return labels.map(label => {
const labelRange = { start: labelStart, end: labelStart + label.length };
const result = matches
.map(match => Range.intersect(labelRange, match))
.filter(range => !Range.isEmpty(range))
.map(({ start, end }) => ({ start: start - labelStart, end: end - labelStart }));
labelStart = labelRange.end + separator.length;
return result;
});
}
class LabelWithHighlights {
private label: string | string[] | undefined = undefined;
private singleLabel: HighlightedLabel | undefined = undefined;
private options: IIconLabelValueOptions | undefined;
constructor(private container: HTMLElement, private supportCodicons: boolean) { }
setLabel(label: string | string[], options?: IIconLabelValueOptions): void {
if (this.label === label && equals(this.options, options)) {
return;
}
this.label = label;
this.options = options;
if (typeof label === 'string') {
if (!this.singleLabel) {
this.container.innerText = '';
this.container.classList.remove('multiple');
this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportCodicons);
}
this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines);
} else {
this.container.innerText = '';
this.container.classList.add('multiple');
this.singleLabel = undefined;
const separator = options?.separator || '/';
const matches = splitMatches(label, separator, options?.matches);
for (let i = 0; i < label.length; i++) {
const l = label[i];
const m = matches ? matches[i] : undefined;
const id = options?.domId && `${options?.domId}_${i}`;
const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' });
const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportCodicons);
highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines);
if (i < label.length - 1) {
dom.append(name, dom.$('span.label-separator', undefined, separator));
}
}
}
}
}

View File

@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* ---------- Icon label ---------- */
.monaco-icon-label {
display: flex; /* required for icons support :before rule */
overflow: hidden;
text-overflow: ellipsis;
}
.monaco-icon-label::before {
/* svg icons rendered as background image */
background-size: 16px;
background-position: left center;
background-repeat: no-repeat;
padding-right: 6px;
width: 16px;
height: 22px;
line-height: inherit !important;
display: inline-block;
/* fonts icons */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
vertical-align: top;
flex-shrink: 0; /* fix for https://github.com/microsoft/vscode/issues/13787 */
}
.monaco-icon-label > .monaco-icon-label-container {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name {
color: inherit;
white-space: pre; /* enable to show labels that include multiple whitespaces */
}
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name > .label-separator {
margin: 0 2px;
opacity: 0.5;
}
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
opacity: .7;
margin-left: 0.5em;
font-size: 0.9em;
white-space: pre; /* enable to show labels that include multiple whitespaces */
}
.vs .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
opacity: .95;
}
.monaco-icon-label.italic > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
.monaco-icon-label.italic > .monaco-icon-description-container > .label-description {
font-style: italic;
}
.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
.monaco-icon-label.strikethrough > .monaco-icon-description-container > .label-description {
text-decoration: line-through;
}
.monaco-icon-label::after {
opacity: 0.75;
font-size: 90%;
font-weight: 600;
padding: 0 16px 0 5px;
text-align: center;
}
/* make sure selection color wins when a label is being selected */
.monaco-list:focus .selected .monaco-icon-label, /* list */
.monaco-list:focus .selected .monaco-icon-label::after
{
color: inherit !important;
}
.monaco-list-row.focused.selected .label-description,
.monaco-list-row.selected .label-description {
opacity: .8;
}

View File

@ -0,0 +1,112 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-inputbox {
position: relative;
display: block;
padding: 0;
box-sizing: border-box;
/* Customizable */
font-size: inherit;
}
.monaco-inputbox.idle {
border: 1px solid transparent;
}
.monaco-inputbox > .wrapper > .input,
.monaco-inputbox > .wrapper > .mirror {
/* Customizable */
padding: 4px;
}
.monaco-inputbox > .wrapper {
position: relative;
width: 100%;
height: 100%;
}
.monaco-inputbox > .wrapper > .input {
display: inline-block;
box-sizing: border-box;
width: 100%;
height: 100%;
line-height: inherit;
border: none;
font-family: inherit;
font-size: inherit;
resize: none;
color: inherit;
}
.monaco-inputbox > .wrapper > input {
text-overflow: ellipsis;
}
.monaco-inputbox > .wrapper > textarea.input {
display: block;
-ms-overflow-style: none; /* IE 10+: hide scrollbars */
scrollbar-width: none; /* Firefox: hide scrollbars */
outline: none;
}
.monaco-inputbox > .wrapper > textarea.input::-webkit-scrollbar {
display: none; /* Chrome + Safari: hide scrollbar */
}
.monaco-inputbox > .wrapper > textarea.input.empty {
white-space: nowrap;
}
.monaco-inputbox > .wrapper > .mirror {
position: absolute;
display: inline-block;
width: 100%;
top: 0;
left: 0;
box-sizing: border-box;
white-space: pre-wrap;
visibility: hidden;
word-wrap: break-word;
}
/* Context view */
.monaco-inputbox-container {
text-align: right;
}
.monaco-inputbox-container .monaco-inputbox-message {
display: inline-block;
overflow: hidden;
text-align: left;
width: 100%;
box-sizing: border-box;
padding: 0.4em;
font-size: 12px;
line-height: 17px;
min-height: 34px;
margin-top: -1px;
word-wrap: break-word;
}
/* Action bar support */
.monaco-inputbox .monaco-action-bar {
position: absolute;
right: 2px;
top: 4px;
}
.monaco-inputbox .monaco-action-bar .action-item {
margin-left: 2px;
}
.monaco-inputbox .monaco-action-bar .action-item .codicon {
background-repeat: no-repeat;
width: 16px;
height: 16px;
}

View File

@ -0,0 +1,687 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./inputBox';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer';
import { renderFormattedText, renderText } from 'vs/base/browser/formattedTextRenderer';
import * as aria from 'vs/base/browser/ui/aria/aria';
import { IAction } from 'vs/base/common/actions';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { IContextViewProvider, AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { Event, Emitter } from 'vs/base/common/event';
import { Widget } from 'vs/base/browser/ui/widget';
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
import { HistoryNavigator } from 'vs/base/common/history';
import { IHistoryNavigationWidget } from 'vs/base/browser/history';
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { domEvent } from 'vs/base/browser/event';
const $ = dom.$;
export interface IInputOptions extends IInputBoxStyles {
readonly placeholder?: string;
readonly ariaLabel?: string;
readonly type?: string;
readonly validationOptions?: IInputValidationOptions;
readonly flexibleHeight?: boolean;
readonly flexibleWidth?: boolean;
readonly flexibleMaxHeight?: number;
readonly actions?: ReadonlyArray<IAction>;
}
export interface IInputBoxStyles {
readonly inputBackground?: Color;
readonly inputForeground?: Color;
readonly inputBorder?: Color;
readonly inputValidationInfoBorder?: Color;
readonly inputValidationInfoBackground?: Color;
readonly inputValidationInfoForeground?: Color;
readonly inputValidationWarningBorder?: Color;
readonly inputValidationWarningBackground?: Color;
readonly inputValidationWarningForeground?: Color;
readonly inputValidationErrorBorder?: Color;
readonly inputValidationErrorBackground?: Color;
readonly inputValidationErrorForeground?: Color;
}
export interface IInputValidator {
(value: string): IMessage | null;
}
export interface IMessage {
readonly content: string;
readonly formatContent?: boolean; // defaults to false
readonly type?: MessageType;
}
export interface IInputValidationOptions {
validation?: IInputValidator;
}
export const enum MessageType {
INFO = 1,
WARNING = 2,
ERROR = 3
}
export interface IRange {
start: number;
end: number;
}
const defaultOpts = {
inputBackground: Color.fromHex('#3C3C3C'),
inputForeground: Color.fromHex('#CCCCCC'),
inputValidationInfoBorder: Color.fromHex('#55AAFF'),
inputValidationInfoBackground: Color.fromHex('#063B49'),
inputValidationWarningBorder: Color.fromHex('#B89500'),
inputValidationWarningBackground: Color.fromHex('#352A05'),
inputValidationErrorBorder: Color.fromHex('#BE1100'),
inputValidationErrorBackground: Color.fromHex('#5A1D1D')
};
export class InputBox extends Widget {
private contextViewProvider?: IContextViewProvider;
element: HTMLElement;
private input: HTMLInputElement;
private actionbar?: ActionBar;
private options: IInputOptions;
private message: IMessage | null;
private placeholder: string;
private ariaLabel: string;
private validation?: IInputValidator;
private state: 'idle' | 'open' | 'closed' = 'idle';
private mirror: HTMLElement | undefined;
private cachedHeight: number | undefined;
private cachedContentHeight: number | undefined;
private maxHeight: number = Number.POSITIVE_INFINITY;
private scrollableElement: ScrollableElement | undefined;
private inputBackground?: Color;
private inputForeground?: Color;
private inputBorder?: Color;
private inputValidationInfoBorder?: Color;
private inputValidationInfoBackground?: Color;
private inputValidationInfoForeground?: Color;
private inputValidationWarningBorder?: Color;
private inputValidationWarningBackground?: Color;
private inputValidationWarningForeground?: Color;
private inputValidationErrorBorder?: Color;
private inputValidationErrorBackground?: Color;
private inputValidationErrorForeground?: Color;
private _onDidChange = this._register(new Emitter<string>());
public readonly onDidChange: Event<string> = this._onDidChange.event;
private _onDidHeightChange = this._register(new Emitter<number>());
public readonly onDidHeightChange: Event<number> = this._onDidHeightChange.event;
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider | undefined, options?: IInputOptions) {
super();
this.contextViewProvider = contextViewProvider;
this.options = options || Object.create(null);
mixin(this.options, defaultOpts, false);
this.message = null;
this.placeholder = this.options.placeholder || '';
this.ariaLabel = this.options.ariaLabel || '';
this.inputBackground = this.options.inputBackground;
this.inputForeground = this.options.inputForeground;
this.inputBorder = this.options.inputBorder;
this.inputValidationInfoBorder = this.options.inputValidationInfoBorder;
this.inputValidationInfoBackground = this.options.inputValidationInfoBackground;
this.inputValidationInfoForeground = this.options.inputValidationInfoForeground;
this.inputValidationWarningBorder = this.options.inputValidationWarningBorder;
this.inputValidationWarningBackground = this.options.inputValidationWarningBackground;
this.inputValidationWarningForeground = this.options.inputValidationWarningForeground;
this.inputValidationErrorBorder = this.options.inputValidationErrorBorder;
this.inputValidationErrorBackground = this.options.inputValidationErrorBackground;
this.inputValidationErrorForeground = this.options.inputValidationErrorForeground;
if (this.options.validationOptions) {
this.validation = this.options.validationOptions.validation;
}
this.element = dom.append(container, $('.monaco-inputbox.idle'));
let tagName = this.options.flexibleHeight ? 'textarea' : 'input';
let wrapper = dom.append(this.element, $('.wrapper'));
this.input = dom.append(wrapper, $(tagName + '.input.empty'));
this.input.setAttribute('autocorrect', 'off');
this.input.setAttribute('autocapitalize', 'off');
this.input.setAttribute('spellcheck', 'false');
this.onfocus(this.input, () => this.element.classList.add('synthetic-focus'));
this.onblur(this.input, () => this.element.classList.remove('synthetic-focus'));
if (this.options.flexibleHeight) {
this.maxHeight = typeof this.options.flexibleMaxHeight === 'number' ? this.options.flexibleMaxHeight : Number.POSITIVE_INFINITY;
this.mirror = dom.append(wrapper, $('div.mirror'));
this.mirror.innerText = '\u00a0';
this.scrollableElement = new ScrollableElement(this.element, { vertical: ScrollbarVisibility.Auto });
if (this.options.flexibleWidth) {
this.input.setAttribute('wrap', 'off');
this.mirror.style.whiteSpace = 'pre';
this.mirror.style.wordWrap = 'initial';
}
dom.append(container, this.scrollableElement.getDomNode());
this._register(this.scrollableElement);
// from ScrollableElement to DOM
this._register(this.scrollableElement.onScroll(e => this.input.scrollTop = e.scrollTop));
const onSelectionChange = Event.filter(domEvent(document, 'selectionchange'), () => {
const selection = document.getSelection();
return selection?.anchorNode === wrapper;
});
// from DOM to ScrollableElement
this._register(onSelectionChange(this.updateScrollDimensions, this));
this._register(this.onDidHeightChange(this.updateScrollDimensions, this));
} else {
this.input.type = this.options.type || 'text';
this.input.setAttribute('wrap', 'off');
}
if (this.ariaLabel) {
this.input.setAttribute('aria-label', this.ariaLabel);
}
if (this.placeholder) {
this.setPlaceHolder(this.placeholder);
}
this.oninput(this.input, () => this.onValueChange());
this.onblur(this.input, () => this.onBlur());
this.onfocus(this.input, () => this.onFocus());
this.ignoreGesture(this.input);
setTimeout(() => this.updateMirror(), 0);
// Support actions
if (this.options.actions) {
this.actionbar = this._register(new ActionBar(this.element));
this.actionbar.push(this.options.actions, { icon: true, label: false });
}
this.applyStyles();
}
private onBlur(): void {
this._hideMessage();
}
private onFocus(): void {
this._showMessage();
}
public setPlaceHolder(placeHolder: string): void {
this.placeholder = placeHolder;
this.input.setAttribute('placeholder', placeHolder);
this.input.title = placeHolder;
}
public setAriaLabel(label: string): void {
this.ariaLabel = label;
if (label) {
this.input.setAttribute('aria-label', this.ariaLabel);
} else {
this.input.removeAttribute('aria-label');
}
}
public getAriaLabel(): string {
return this.ariaLabel;
}
public get mirrorElement(): HTMLElement | undefined {
return this.mirror;
}
public get inputElement(): HTMLInputElement {
return this.input;
}
public get value(): string {
return this.input.value;
}
public set value(newValue: string) {
if (this.input.value !== newValue) {
this.input.value = newValue;
this.onValueChange();
}
}
public get height(): number {
return typeof this.cachedHeight === 'number' ? this.cachedHeight : dom.getTotalHeight(this.element);
}
public focus(): void {
this.input.focus();
}
public blur(): void {
this.input.blur();
}
public hasFocus(): boolean {
return document.activeElement === this.input;
}
public select(range: IRange | null = null): void {
this.input.select();
if (range) {
this.input.setSelectionRange(range.start, range.end);
}
}
public isSelectionAtEnd(): boolean {
return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd;
}
public enable(): void {
this.input.removeAttribute('disabled');
}
public disable(): void {
this.blur();
this.input.disabled = true;
this._hideMessage();
}
public setEnabled(enabled: boolean): void {
if (enabled) {
this.enable();
} else {
this.disable();
}
}
public get width(): number {
return dom.getTotalWidth(this.input);
}
public set width(width: number) {
if (this.options.flexibleHeight && this.options.flexibleWidth) {
// textarea with horizontal scrolling
let horizontalPadding = 0;
if (this.mirror) {
const paddingLeft = parseFloat(this.mirror.style.paddingLeft || '') || 0;
const paddingRight = parseFloat(this.mirror.style.paddingRight || '') || 0;
horizontalPadding = paddingLeft + paddingRight;
}
this.input.style.width = (width - horizontalPadding) + 'px';
} else {
this.input.style.width = width + 'px';
}
if (this.mirror) {
this.mirror.style.width = width + 'px';
}
}
public set paddingRight(paddingRight: number) {
if (this.options.flexibleHeight && this.options.flexibleWidth) {
this.input.style.width = `calc(100% - ${paddingRight}px)`;
} else {
this.input.style.paddingRight = paddingRight + 'px';
}
if (this.mirror) {
this.mirror.style.paddingRight = paddingRight + 'px';
}
}
private updateScrollDimensions(): void {
if (typeof this.cachedContentHeight !== 'number' || typeof this.cachedHeight !== 'number' || !this.scrollableElement) {
return;
}
const scrollHeight = this.cachedContentHeight;
const height = this.cachedHeight;
const scrollTop = this.input.scrollTop;
this.scrollableElement.setScrollDimensions({ scrollHeight, height });
this.scrollableElement.setScrollPosition({ scrollTop });
}
public showMessage(message: IMessage, force?: boolean): void {
this.message = message;
this.element.classList.remove('idle');
this.element.classList.remove('info');
this.element.classList.remove('warning');
this.element.classList.remove('error');
this.element.classList.add(this.classForType(message.type));
const styles = this.stylesForType(this.message.type);
this.element.style.border = styles.border ? `1px solid ${styles.border}` : '';
if (this.hasFocus() || force) {
this._showMessage();
}
}
public hideMessage(): void {
this.message = null;
this.element.classList.remove('info');
this.element.classList.remove('warning');
this.element.classList.remove('error');
this.element.classList.add('idle');
this._hideMessage();
this.applyStyles();
}
public isInputValid(): boolean {
return !!this.validation && !this.validation(this.value);
}
public validate(): boolean {
let errorMsg: IMessage | null = null;
if (this.validation) {
errorMsg = this.validation(this.value);
if (errorMsg) {
this.inputElement.setAttribute('aria-invalid', 'true');
this.showMessage(errorMsg);
}
else if (this.inputElement.hasAttribute('aria-invalid')) {
this.inputElement.removeAttribute('aria-invalid');
this.hideMessage();
}
}
return !errorMsg;
}
public stylesForType(type: MessageType | undefined): { border: Color | undefined; background: Color | undefined; foreground: Color | undefined } {
switch (type) {
case MessageType.INFO: return { border: this.inputValidationInfoBorder, background: this.inputValidationInfoBackground, foreground: this.inputValidationInfoForeground };
case MessageType.WARNING: return { border: this.inputValidationWarningBorder, background: this.inputValidationWarningBackground, foreground: this.inputValidationWarningForeground };
default: return { border: this.inputValidationErrorBorder, background: this.inputValidationErrorBackground, foreground: this.inputValidationErrorForeground };
}
}
private classForType(type: MessageType | undefined): string {
switch (type) {
case MessageType.INFO: return 'info';
case MessageType.WARNING: return 'warning';
default: return 'error';
}
}
private _showMessage(): void {
if (!this.contextViewProvider || !this.message) {
return;
}
let div: HTMLElement;
let layout = () => div.style.width = dom.getTotalWidth(this.element) + 'px';
this.contextViewProvider.showContextView({
getAnchor: () => this.element,
anchorAlignment: AnchorAlignment.RIGHT,
render: (container: HTMLElement) => {
if (!this.message) {
return null;
}
div = dom.append(container, $('.monaco-inputbox-container'));
layout();
const renderOptions: MarkdownRenderOptions = {
inline: true,
className: 'monaco-inputbox-message'
};
const spanElement = (this.message.formatContent
? renderFormattedText(this.message.content, renderOptions)
: renderText(this.message.content, renderOptions));
spanElement.classList.add(this.classForType(this.message.type));
const styles = this.stylesForType(this.message.type);
spanElement.style.backgroundColor = styles.background ? styles.background.toString() : '';
spanElement.style.color = styles.foreground ? styles.foreground.toString() : '';
spanElement.style.border = styles.border ? `1px solid ${styles.border}` : '';
dom.append(div, spanElement);
return null;
},
onHide: () => {
this.state = 'closed';
},
layout: layout
});
// ARIA Support
let alertText: string;
if (this.message.type === MessageType.ERROR) {
alertText = nls.localize('alertErrorMessage', "Error: {0}", this.message.content);
} else if (this.message.type === MessageType.WARNING) {
alertText = nls.localize('alertWarningMessage', "Warning: {0}", this.message.content);
} else {
alertText = nls.localize('alertInfoMessage', "Info: {0}", this.message.content);
}
aria.alert(alertText);
this.state = 'open';
}
private _hideMessage(): void {
if (!this.contextViewProvider) {
return;
}
if (this.state === 'open') {
this.contextViewProvider.hideContextView();
}
this.state = 'idle';
}
private onValueChange(): void {
this._onDidChange.fire(this.value);
this.validate();
this.updateMirror();
this.input.classList.toggle('empty', !this.value);
if (this.state === 'open' && this.contextViewProvider) {
this.contextViewProvider.layout();
}
}
private updateMirror(): void {
if (!this.mirror) {
return;
}
const value = this.value;
const lastCharCode = value.charCodeAt(value.length - 1);
const suffix = lastCharCode === 10 ? ' ' : '';
const mirrorTextContent = value + suffix;
if (mirrorTextContent) {
this.mirror.textContent = value + suffix;
} else {
this.mirror.innerText = '\u00a0';
}
this.layout();
}
public style(styles: IInputBoxStyles): void {
this.inputBackground = styles.inputBackground;
this.inputForeground = styles.inputForeground;
this.inputBorder = styles.inputBorder;
this.inputValidationInfoBackground = styles.inputValidationInfoBackground;
this.inputValidationInfoForeground = styles.inputValidationInfoForeground;
this.inputValidationInfoBorder = styles.inputValidationInfoBorder;
this.inputValidationWarningBackground = styles.inputValidationWarningBackground;
this.inputValidationWarningForeground = styles.inputValidationWarningForeground;
this.inputValidationWarningBorder = styles.inputValidationWarningBorder;
this.inputValidationErrorBackground = styles.inputValidationErrorBackground;
this.inputValidationErrorForeground = styles.inputValidationErrorForeground;
this.inputValidationErrorBorder = styles.inputValidationErrorBorder;
this.applyStyles();
}
protected applyStyles(): void {
const background = this.inputBackground ? this.inputBackground.toString() : '';
const foreground = this.inputForeground ? this.inputForeground.toString() : '';
const border = this.inputBorder ? this.inputBorder.toString() : '';
this.element.style.backgroundColor = background;
this.element.style.color = foreground;
this.input.style.backgroundColor = 'inherit';
this.input.style.color = foreground;
this.element.style.borderWidth = border ? '1px' : '';
this.element.style.borderStyle = border ? 'solid' : '';
this.element.style.borderColor = border;
}
public layout(): void {
if (!this.mirror) {
return;
}
const previousHeight = this.cachedContentHeight;
this.cachedContentHeight = dom.getTotalHeight(this.mirror);
if (previousHeight !== this.cachedContentHeight) {
this.cachedHeight = Math.min(this.cachedContentHeight, this.maxHeight);
this.input.style.height = this.cachedHeight + 'px';
this._onDidHeightChange.fire(this.cachedContentHeight);
}
}
public insertAtCursor(text: string): void {
const inputElement = this.inputElement;
const start = inputElement.selectionStart;
const end = inputElement.selectionEnd;
const content = inputElement.value;
if (start !== null && end !== null) {
this.value = content.substr(0, start) + text + content.substr(end);
inputElement.setSelectionRange(start + 1, start + 1);
this.layout();
}
}
public dispose(): void {
this._hideMessage();
this.message = null;
if (this.actionbar) {
this.actionbar.dispose();
}
super.dispose();
}
}
export interface IHistoryInputOptions extends IInputOptions {
history: string[];
}
export class HistoryInputBox extends InputBox implements IHistoryNavigationWidget {
private readonly history: HistoryNavigator<string>;
constructor(container: HTMLElement, contextViewProvider: IContextViewProvider | undefined, options: IHistoryInputOptions) {
super(container, contextViewProvider, options);
this.history = new HistoryNavigator<string>(options.history, 100);
}
public addToHistory(): void {
if (this.value && this.value !== this.getCurrentValue()) {
this.history.add(this.value);
}
}
public getHistory(): string[] {
return this.history.getHistory();
}
public showNextValue(): void {
if (!this.history.has(this.value)) {
this.addToHistory();
}
let next = this.getNextValue();
if (next) {
next = next === this.value ? this.getNextValue() : next;
}
if (next) {
this.value = next;
aria.status(this.value);
}
}
public showPreviousValue(): void {
if (!this.history.has(this.value)) {
this.addToHistory();
}
let previous = this.getPreviousValue();
if (previous) {
previous = previous === this.value ? this.getPreviousValue() : previous;
}
if (previous) {
this.value = previous;
aria.status(this.value);
}
}
public clearHistory(): void {
this.history.clear();
}
private getCurrentValue(): string | null {
let currentValue = this.history.current();
if (!currentValue) {
currentValue = this.history.last();
this.history.next();
}
return currentValue;
}
private getPreviousValue(): string | null {
return this.history.previous() || this.history.first();
}
private getNextValue(): string | null {
return this.history.next() || this.history.last();
}
}

View File

@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-keybinding {
display: flex;
align-items: center;
line-height: 10px;
}
.monaco-keybinding > .monaco-keybinding-key {
display: inline-block;
border: solid 1px rgba(204, 204, 204, 0.4);
border-bottom-color: rgba(187, 187, 187, 0.4);
border-radius: 3px;
box-shadow: inset 0 -1px 0 rgba(187, 187, 187, 0.4);
background-color: rgba(221, 221, 221, 0.4);
vertical-align: middle;
color: #555;
font-size: 11px;
padding: 3px 5px;
margin: 0 2px;
}
.monaco-keybinding > .monaco-keybinding-key:first-child {
margin-left: 0;
}
.monaco-keybinding > .monaco-keybinding-key:last-child {
margin-right: 0;
}
.hc-black .monaco-keybinding > .monaco-keybinding-key,
.vs-dark .monaco-keybinding > .monaco-keybinding-key {
background-color: rgba(128, 128, 128, 0.17);
color: #ccc;
border: solid 1px rgba(51, 51, 51, 0.6);
border-bottom-color: rgba(68, 68, 68, 0.6);
box-shadow: inset 0 -1px 0 rgba(68, 68, 68, 0.6);
}
.monaco-keybinding > .monaco-keybinding-key-separator {
display: inline-block;
}
.monaco-keybinding > .monaco-keybinding-key-chord-separator {
width: 6px;
}

View File

@ -0,0 +1,117 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./keybindingLabel';
import { equals } from 'vs/base/common/objects';
import { OperatingSystem } from 'vs/base/common/platform';
import { ResolvedKeybinding, ResolvedKeybindingPart } from 'vs/base/common/keyCodes';
import { UILabelProvider } from 'vs/base/common/keybindingLabels';
import * as dom from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
const $ = dom.$;
export interface PartMatches {
ctrlKey?: boolean;
shiftKey?: boolean;
altKey?: boolean;
metaKey?: boolean;
keyCode?: boolean;
}
export interface Matches {
firstPart: PartMatches;
chordPart: PartMatches;
}
export interface KeybindingLabelOptions {
renderUnboundKeybindings: boolean;
}
export class KeybindingLabel {
private domNode: HTMLElement;
private keybinding: ResolvedKeybinding | undefined;
private matches: Matches | undefined;
private didEverRender: boolean;
constructor(container: HTMLElement, private os: OperatingSystem, private options?: KeybindingLabelOptions) {
this.domNode = dom.append(container, $('.monaco-keybinding'));
this.didEverRender = false;
container.appendChild(this.domNode);
}
get element(): HTMLElement {
return this.domNode;
}
set(keybinding: ResolvedKeybinding | undefined, matches?: Matches) {
if (this.didEverRender && this.keybinding === keybinding && KeybindingLabel.areSame(this.matches, matches)) {
return;
}
this.keybinding = keybinding;
this.matches = matches;
this.render();
}
private render() {
dom.clearNode(this.domNode);
if (this.keybinding) {
let [firstPart, chordPart] = this.keybinding.getParts();
if (firstPart) {
this.renderPart(this.domNode, firstPart, this.matches ? this.matches.firstPart : null);
}
if (chordPart) {
dom.append(this.domNode, $('span.monaco-keybinding-key-chord-separator', undefined, ' '));
this.renderPart(this.domNode, chordPart, this.matches ? this.matches.chordPart : null);
}
this.domNode.title = this.keybinding.getAriaLabel() || '';
} else if (this.options && this.options.renderUnboundKeybindings) {
this.renderUnbound(this.domNode);
}
this.didEverRender = true;
}
private renderPart(parent: HTMLElement, part: ResolvedKeybindingPart, match: PartMatches | null) {
const modifierLabels = UILabelProvider.modifierLabels[this.os];
if (part.ctrlKey) {
this.renderKey(parent, modifierLabels.ctrlKey, Boolean(match?.ctrlKey), modifierLabels.separator);
}
if (part.shiftKey) {
this.renderKey(parent, modifierLabels.shiftKey, Boolean(match?.shiftKey), modifierLabels.separator);
}
if (part.altKey) {
this.renderKey(parent, modifierLabels.altKey, Boolean(match?.altKey), modifierLabels.separator);
}
if (part.metaKey) {
this.renderKey(parent, modifierLabels.metaKey, Boolean(match?.metaKey), modifierLabels.separator);
}
const keyLabel = part.keyLabel;
if (keyLabel) {
this.renderKey(parent, keyLabel, Boolean(match?.keyCode), '');
}
}
private renderKey(parent: HTMLElement, label: string, highlight: boolean, separator: string): void {
dom.append(parent, $('span.monaco-keybinding-key' + (highlight ? '.highlight' : ''), undefined, label));
if (separator) {
dom.append(parent, $('span.monaco-keybinding-key-separator', undefined, separator));
}
}
private renderUnbound(parent: HTMLElement): void {
dom.append(parent, $('span.monaco-keybinding-key', undefined, localize('unbound', "Unbound")));
}
private static areSame(a: Matches | undefined, b: Matches | undefined): boolean {
if (a === b || (!a && !b)) {
return true;
}
return !!a && !!b && equals(a.firstPart, b.firstPart) && equals(a.chordPart, b.chordPart);
}
}

View File

@ -0,0 +1,162 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-list {
position: relative;
height: 100%;
width: 100%;
white-space: nowrap;
}
.monaco-list.mouse-support {
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.monaco-list > .monaco-scrollable-element {
height: 100%;
}
.monaco-list-rows {
position: relative;
width: 100%;
height: 100%;
}
.monaco-list.horizontal-scrolling .monaco-list-rows {
width: auto;
min-width: 100%;
}
.monaco-list-row {
position: absolute;
box-sizing: border-box;
overflow: hidden;
width: 100%;
}
.monaco-list.mouse-support .monaco-list-row {
cursor: pointer;
touch-action: none;
}
/* for OS X ballistic scrolling */
.monaco-list-row.scrolling {
display: none !important;
}
/* Focus */
.monaco-list.element-focused, .monaco-list.selection-single, .monaco-list.selection-multiple {
outline: 0 !important;
}
.monaco-list:focus .monaco-list-row.selected .codicon {
color: inherit;
}
/* Dnd */
.monaco-drag-image {
display: inline-block;
padding: 1px 7px;
border-radius: 10px;
font-size: 12px;
position: absolute;
}
/* Type filter */
.monaco-list-type-filter {
display: flex;
align-items: center;
position: absolute;
border-radius: 2px;
padding: 0px 3px;
max-width: calc(100% - 10px);
text-overflow: ellipsis;
overflow: hidden;
text-align: right;
box-sizing: border-box;
cursor: all-scroll;
font-size: 13px;
line-height: 18px;
height: 20px;
z-index: 1;
top: 4px;
}
.monaco-list-type-filter.dragging {
transition: top 0.2s, left 0.2s;
}
.monaco-list-type-filter.ne {
right: 4px;
}
.monaco-list-type-filter.nw {
left: 4px;
}
.monaco-list-type-filter > .controls {
display: flex;
align-items: center;
box-sizing: border-box;
transition: width 0.2s;
width: 0;
}
.monaco-list-type-filter.dragging > .controls,
.monaco-list-type-filter:hover > .controls {
width: 36px;
}
.monaco-list-type-filter > .controls > * {
border: none;
box-sizing: border-box;
-webkit-appearance: none;
-moz-appearance: none;
background: none;
width: 16px;
height: 16px;
flex-shrink: 0;
margin: 0;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.monaco-list-type-filter > .controls > .filter {
margin-left: 4px;
}
.monaco-list-type-filter-message {
position: absolute;
box-sizing: border-box;
width: 100%;
height: 100%;
top: 0;
left: 0;
padding: 40px 1em 1em 1em;
text-align: center;
white-space: normal;
opacity: 0.7;
pointer-events: none;
}
.monaco-list-type-filter-message:empty {
display: none;
}
/* Electron */
.monaco-list-type-filter {
cursor: grab;
}
.monaco-list-type-filter.dragging {
cursor: grabbing;
}

View File

@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { GestureEvent } from 'vs/base/browser/touch';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IDragAndDropData } from 'vs/base/browser/dnd';
export interface IListVirtualDelegate<T> {
getHeight(element: T): number;
getTemplateId(element: T): string;
hasDynamicHeight?(element: T): boolean;
setDynamicHeight?(element: T, height: number): void;
}
export interface IListRenderer<T, TTemplateData> {
templateId: string;
renderTemplate(container: HTMLElement): TTemplateData;
renderElement(element: T, index: number, templateData: TTemplateData, height: number | undefined): void;
disposeElement?(element: T, index: number, templateData: TTemplateData, height: number | undefined): void;
disposeTemplate(templateData: TTemplateData): void;
}
export interface IListEvent<T> {
elements: T[];
indexes: number[];
browserEvent?: UIEvent;
}
export interface IListMouseEvent<T> {
browserEvent: MouseEvent;
element: T | undefined;
index: number | undefined;
}
export interface IListTouchEvent<T> {
browserEvent: TouchEvent;
element: T | undefined;
index: number | undefined;
}
export interface IListGestureEvent<T> {
browserEvent: GestureEvent;
element: T | undefined;
index: number | undefined;
}
export interface IListDragEvent<T> {
browserEvent: DragEvent;
element: T | undefined;
index: number | undefined;
}
export interface IListContextMenuEvent<T> {
browserEvent: UIEvent;
element: T | undefined;
index: number | undefined;
anchor: HTMLElement | { x: number; y: number; };
}
export interface IIdentityProvider<T> {
getId(element: T): { toString(): string; };
}
export interface IKeyboardNavigationLabelProvider<T> {
/**
* Return a keyboard navigation label(s) which will be used by
* the list for filtering/navigating. Return `undefined` to make
* an element always match.
*/
getKeyboardNavigationLabel(element: T): { toString(): string | undefined; } | { toString(): string | undefined; }[] | undefined;
}
export interface IKeyboardNavigationDelegate {
mightProducePrintableCharacter(event: IKeyboardEvent): boolean;
}
export const enum ListDragOverEffect {
Copy,
Move
}
export interface IListDragOverReaction {
accept: boolean;
effect?: ListDragOverEffect;
feedback?: number[]; // use -1 for entire list
}
export const ListDragOverReactions = {
reject(): IListDragOverReaction { return { accept: false }; },
accept(): IListDragOverReaction { return { accept: true }; },
};
export interface IListDragAndDrop<T> {
getDragURI(element: T): string | null;
getDragLabel?(elements: T[], originalEvent: DragEvent): string | undefined;
onDragStart?(data: IDragAndDropData, originalEvent: DragEvent): void;
onDragOver(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | IListDragOverReaction;
drop(data: IDragAndDropData, targetElement: T | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void;
onDragEnd?(originalEvent: DragEvent): void;
}
export class ListError extends Error {
constructor(user: string, message: string) {
super(`ListError [${user}] ${message}`);
}
}
export abstract class CachedListVirtualDelegate<T extends object> implements IListVirtualDelegate<T> {
private cache = new WeakMap<T, number>();
getHeight(element: T): number {
return this.cache.get(element) ?? this.estimateHeight(element);
}
protected abstract estimateHeight(element: T): number;
abstract getTemplateId(element: T): string;
setDynamicHeight(element: T, height: number): void {
if (height > 0) {
this.cache.set(element, height);
}
}
}

View File

@ -0,0 +1,280 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./list';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { range } from 'vs/base/common/arrays';
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent } from './list';
import { List, IListStyles, IListOptions, IListAccessibilityProvider, IListOptionsUpdate } from './listWidget';
import { IPagedModel } from 'vs/base/common/paging';
import { Event } from 'vs/base/common/event';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { IThemable } from 'vs/base/common/styler';
export interface IPagedRenderer<TElement, TTemplateData> extends IListRenderer<TElement, TTemplateData> {
renderPlaceholder(index: number, templateData: TTemplateData): void;
}
export interface ITemplateData<T> {
data?: T;
disposable?: IDisposable;
}
class PagedRenderer<TElement, TTemplateData> implements IListRenderer<number, ITemplateData<TTemplateData>> {
get templateId(): string { return this.renderer.templateId; }
constructor(
private renderer: IPagedRenderer<TElement, TTemplateData>,
private modelProvider: () => IPagedModel<TElement>
) { }
renderTemplate(container: HTMLElement): ITemplateData<TTemplateData> {
const data = this.renderer.renderTemplate(container);
return { data, disposable: Disposable.None };
}
renderElement(index: number, _: number, data: ITemplateData<TTemplateData>, height: number | undefined): void {
if (data.disposable) {
data.disposable.dispose();
}
if (!data.data) {
return;
}
const model = this.modelProvider();
if (model.isResolved(index)) {
return this.renderer.renderElement(model.get(index), index, data.data, height);
}
const cts = new CancellationTokenSource();
const promise = model.resolve(index, cts.token);
data.disposable = { dispose: () => cts.cancel() };
this.renderer.renderPlaceholder(index, data.data);
promise.then(entry => this.renderer.renderElement(entry, index, data.data!, height));
}
disposeTemplate(data: ITemplateData<TTemplateData>): void {
if (data.disposable) {
data.disposable.dispose();
data.disposable = undefined;
}
if (data.data) {
this.renderer.disposeTemplate(data.data);
data.data = undefined;
}
}
}
class PagedAccessibilityProvider<T> implements IListAccessibilityProvider<number> {
constructor(
private modelProvider: () => IPagedModel<T>,
private accessibilityProvider: IListAccessibilityProvider<T>
) { }
getWidgetAriaLabel(): string {
return this.accessibilityProvider.getWidgetAriaLabel();
}
getAriaLabel(index: number): string | null {
const model = this.modelProvider();
if (!model.isResolved(index)) {
return null;
}
return this.accessibilityProvider.getAriaLabel(model.get(index));
}
}
export interface IPagedListOptions<T> {
readonly enableKeyboardNavigation?: boolean;
readonly automaticKeyboardNavigation?: boolean;
readonly ariaLabel?: string;
readonly keyboardSupport?: boolean;
readonly multipleSelectionSupport?: boolean;
readonly accessibilityProvider?: IListAccessibilityProvider<T>;
// list view options
readonly useShadows?: boolean;
readonly verticalScrollMode?: ScrollbarVisibility;
readonly setRowLineHeight?: boolean;
readonly setRowHeight?: boolean;
readonly supportDynamicHeights?: boolean;
readonly mouseSupport?: boolean;
readonly horizontalScrolling?: boolean;
readonly additionalScrollHeight?: number;
}
function fromPagedListOptions<T>(modelProvider: () => IPagedModel<T>, options: IPagedListOptions<T>): IListOptions<number> {
return {
...options,
accessibilityProvider: options.accessibilityProvider && new PagedAccessibilityProvider(modelProvider, options.accessibilityProvider)
};
}
export class PagedList<T> implements IThemable, IDisposable {
private list: List<number>;
private _model!: IPagedModel<T>;
constructor(
user: string,
container: HTMLElement,
virtualDelegate: IListVirtualDelegate<number>,
renderers: IPagedRenderer<T, any>[],
options: IPagedListOptions<T> = {}
) {
const modelProvider = () => this.model;
const pagedRenderers = renderers.map(r => new PagedRenderer<T, ITemplateData<T>>(r, modelProvider));
this.list = new List(user, container, virtualDelegate, pagedRenderers, fromPagedListOptions(modelProvider, options));
}
updateOptions(options: IListOptionsUpdate) {
this.list.updateOptions(options);
}
getHTMLElement(): HTMLElement {
return this.list.getHTMLElement();
}
isDOMFocused(): boolean {
return this.list.getHTMLElement() === document.activeElement;
}
domFocus(): void {
this.list.domFocus();
}
get onDidFocus(): Event<void> {
return this.list.onDidFocus;
}
get onDidBlur(): Event<void> {
return this.list.onDidBlur;
}
get widget(): List<number> {
return this.list;
}
get onDidDispose(): Event<void> {
return this.list.onDidDispose;
}
get onMouseClick(): Event<IListMouseEvent<T>> {
return Event.map(this.list.onMouseClick, ({ element, index, browserEvent }) => ({ element: element === undefined ? undefined : this._model.get(element), index, browserEvent }));
}
get onMouseDblClick(): Event<IListMouseEvent<T>> {
return Event.map(this.list.onMouseDblClick, ({ element, index, browserEvent }) => ({ element: element === undefined ? undefined : this._model.get(element), index, browserEvent }));
}
get onTap(): Event<IListMouseEvent<T>> {
return Event.map(this.list.onTap, ({ element, index, browserEvent }) => ({ element: element === undefined ? undefined : this._model.get(element), index, browserEvent }));
}
get onPointer(): Event<IListMouseEvent<T>> {
return Event.map(this.list.onPointer, ({ element, index, browserEvent }) => ({ element: element === undefined ? undefined : this._model.get(element), index, browserEvent }));
}
get onDidChangeFocus(): Event<IListEvent<T>> {
return Event.map(this.list.onDidChangeFocus, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent }));
}
get onDidChangeSelection(): Event<IListEvent<T>> {
return Event.map(this.list.onDidChangeSelection, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent }));
}
get onContextMenu(): Event<IListContextMenuEvent<T>> {
return Event.map(this.list.onContextMenu, ({ element, index, anchor, browserEvent }) => (typeof element === 'undefined' ? { element, index, anchor, browserEvent } : { element: this._model.get(element), index, anchor, browserEvent }));
}
get model(): IPagedModel<T> {
return this._model;
}
set model(model: IPagedModel<T>) {
this._model = model;
this.list.splice(0, this.list.length, range(model.length));
}
get length(): number {
return this.list.length;
}
get scrollTop(): number {
return this.list.scrollTop;
}
set scrollTop(scrollTop: number) {
this.list.scrollTop = scrollTop;
}
get scrollLeft(): number {
return this.list.scrollLeft;
}
set scrollLeft(scrollLeft: number) {
this.list.scrollLeft = scrollLeft;
}
setFocus(indexes: number[]): void {
this.list.setFocus(indexes);
}
focusNext(n?: number, loop?: boolean): void {
this.list.focusNext(n, loop);
}
focusPrevious(n?: number, loop?: boolean): void {
this.list.focusPrevious(n, loop);
}
focusNextPage(): void {
this.list.focusNextPage();
}
focusPreviousPage(): void {
this.list.focusPreviousPage();
}
getFocus(): number[] {
return this.list.getFocus();
}
setSelection(indexes: number[], browserEvent?: UIEvent): void {
this.list.setSelection(indexes, browserEvent);
}
getSelection(): number[] {
return this.list.getSelection();
}
layout(height?: number, width?: number): void {
this.list.layout(height, width);
}
toggleKeyboardNavigation(): void {
this.list.toggleKeyboardNavigation();
}
reveal(index: number, relativeTop?: number): void {
this.list.reveal(index, relativeTop);
}
style(styles: IListStyles): void {
this.list.style(styles);
}
dispose(): void {
this.list.dispose();
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,189 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IRange, Range } from 'vs/base/common/range';
export interface IItem {
size: number;
}
export interface IRangedGroup {
range: IRange;
size: number;
}
/**
* Returns the intersection between a ranged group and a range.
* Returns `[]` if the intersection is empty.
*/
export function groupIntersect(range: IRange, groups: IRangedGroup[]): IRangedGroup[] {
const result: IRangedGroup[] = [];
for (let r of groups) {
if (range.start >= r.range.end) {
continue;
}
if (range.end < r.range.start) {
break;
}
const intersection = Range.intersect(range, r.range);
if (Range.isEmpty(intersection)) {
continue;
}
result.push({
range: intersection,
size: r.size
});
}
return result;
}
/**
* Shifts a range by that `much`.
*/
export function shift({ start, end }: IRange, much: number): IRange {
return { start: start + much, end: end + much };
}
/**
* Consolidates a collection of ranged groups.
*
* Consolidation is the process of merging consecutive ranged groups
* that share the same `size`.
*/
export function consolidate(groups: IRangedGroup[]): IRangedGroup[] {
const result: IRangedGroup[] = [];
let previousGroup: IRangedGroup | null = null;
for (let group of groups) {
const start = group.range.start;
const end = group.range.end;
const size = group.size;
if (previousGroup && size === previousGroup.size) {
previousGroup.range.end = end;
continue;
}
previousGroup = { range: { start, end }, size };
result.push(previousGroup);
}
return result;
}
/**
* Concatenates several collections of ranged groups into a single
* collection.
*/
function concat(...groups: IRangedGroup[][]): IRangedGroup[] {
return consolidate(groups.reduce((r, g) => r.concat(g), []));
}
export class RangeMap {
private groups: IRangedGroup[] = [];
private _size = 0;
splice(index: number, deleteCount: number, items: IItem[] = []): void {
const diff = items.length - deleteCount;
const before = groupIntersect({ start: 0, end: index }, this.groups);
const after = groupIntersect({ start: index + deleteCount, end: Number.POSITIVE_INFINITY }, this.groups)
.map<IRangedGroup>(g => ({ range: shift(g.range, diff), size: g.size }));
const middle = items.map<IRangedGroup>((item, i) => ({
range: { start: index + i, end: index + i + 1 },
size: item.size
}));
this.groups = concat(before, middle, after);
this._size = this.groups.reduce((t, g) => t + (g.size * (g.range.end - g.range.start)), 0);
}
/**
* Returns the number of items in the range map.
*/
get count(): number {
const len = this.groups.length;
if (!len) {
return 0;
}
return this.groups[len - 1].range.end;
}
/**
* Returns the sum of the sizes of all items in the range map.
*/
get size(): number {
return this._size;
}
/**
* Returns the index of the item at the given position.
*/
indexAt(position: number): number {
if (position < 0) {
return -1;
}
let index = 0;
let size = 0;
for (let group of this.groups) {
const count = group.range.end - group.range.start;
const newSize = size + (count * group.size);
if (position < newSize) {
return index + Math.floor((position - size) / group.size);
}
index += count;
size = newSize;
}
return index;
}
/**
* Returns the index of the item right after the item at the
* index of the given position.
*/
indexAfter(position: number): number {
return Math.min(this.indexAt(position) + 1, this.count);
}
/**
* Returns the start position of the item at the given index.
*/
positionAt(index: number): number {
if (index < 0) {
return -1;
}
let position = 0;
let count = 0;
for (let group of this.groups) {
const groupCount = group.range.end - group.range.start;
const newCount = count + groupCount;
if (index < newCount) {
return position + ((index - count) * group.size);
}
position += groupCount * group.size;
count = newCount;
}
return -1;
}
}

View File

@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IListRenderer } from './list';
import { IDisposable } from 'vs/base/common/lifecycle';
import { $ } from 'vs/base/browser/dom';
export interface IRow {
domNode: HTMLElement | null;
templateId: string;
templateData: any;
}
function removeFromParent(element: HTMLElement): void {
try {
if (element.parentElement) {
element.parentElement.removeChild(element);
}
} catch (e) {
// this will throw if this happens due to a blur event, nasty business
}
}
export class RowCache<T> implements IDisposable {
private cache = new Map<string, IRow[]>();
constructor(private renderers: Map<string, IListRenderer<T, any>>) { }
/**
* Returns a row either by creating a new one or reusing
* a previously released row which shares the same templateId.
*/
alloc(templateId: string): IRow {
let result = this.getTemplateCache(templateId).pop();
if (!result) {
const domNode = $('.monaco-list-row');
const renderer = this.getRenderer(templateId);
const templateData = renderer.renderTemplate(domNode);
result = { domNode, templateId, templateData };
}
return result;
}
/**
* Releases the row for eventual reuse.
*/
release(row: IRow): void {
if (!row) {
return;
}
this.releaseRow(row);
}
private releaseRow(row: IRow): void {
const { domNode, templateId } = row;
if (domNode) {
domNode.classList.remove('scrolling');
removeFromParent(domNode);
}
const cache = this.getTemplateCache(templateId);
cache.push(row);
}
private getTemplateCache(templateId: string): IRow[] {
let result = this.cache.get(templateId);
if (!result) {
result = [];
this.cache.set(templateId, result);
}
return result;
}
dispose(): void {
this.cache.forEach((cachedRows, templateId) => {
for (const cachedRow of cachedRows) {
const renderer = this.getRenderer(templateId);
renderer.disposeTemplate(cachedRow.templateData);
cachedRow.domNode = null;
cachedRow.templateData = null;
}
});
this.cache.clear();
}
private getRenderer(templateId: string): IListRenderer<T, any> {
const renderer = this.renderers.get(templateId);
if (!renderer) {
throw new Error(`No renderer found for ${templateId}`);
}
return renderer;
}
}

View File

@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ISpliceable } from 'vs/base/common/sequence';
export interface ISpreadSpliceable<T> {
splice(start: number, deleteCount: number, ...elements: T[]): void;
}
export class CombinedSpliceable<T> implements ISpliceable<T> {
constructor(private spliceables: ISpliceable<T>[]) { }
splice(start: number, deleteCount: number, elements: T[]): void {
this.spliceables.forEach(s => s.splice(start, deleteCount, elements));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Menubar styles */
.menubar {
display: flex;
flex-shrink: 1;
box-sizing: border-box;
height: 30px;
overflow: hidden;
flex-wrap: wrap;
}
.fullscreen .menubar:not(.compact) {
margin: 0px;
padding: 0px 5px;
}
.menubar > .menubar-menu-button {
align-items: center;
box-sizing: border-box;
padding: 0px 8px;
cursor: default;
-webkit-app-region: no-drag;
zoom: 1;
white-space: nowrap;
outline: 0;
}
.menubar.compact {
flex-shrink: 0;
overflow: visible; /* to avoid the compact menu to be repositioned when clicking */
}
.menubar.compact > .menubar-menu-button {
width: 100%;
height: 100%;
padding: 0px;
}
.menubar .menubar-menu-items-holder {
position: absolute;
left: 0px;
opacity: 1;
z-index: 2000;
}
.menubar.compact .menubar-menu-items-holder {
position: fixed;
}
.menubar .menubar-menu-items-holder.monaco-menu-container {
outline: 0;
border: none;
}
.menubar .menubar-menu-items-holder.monaco-menu-container :focus {
outline: 0;
}
.menubar .toolbar-toggle-more {
width: 20px;
height: 100%;
}
.menubar.compact .toolbar-toggle-more {
position: relative;
left: 0px;
top: 0px;
cursor: pointer;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.menubar .toolbar-toggle-more {
padding: 0;
vertical-align: sub;
}
.menubar.compact .toolbar-toggle-more::before {
content: "\eb94" !important;
}

View File

@ -0,0 +1,997 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./menubar';
import * as browser from 'vs/base/browser/browser';
import * as DOM from 'vs/base/browser/dom';
import * as strings from 'vs/base/common/strings';
import * as nls from 'vs/nls';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch';
import { cleanMnemonic, IMenuOptions, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX, IMenuStyles, Direction } from 'vs/base/browser/ui/menu/menu';
import { ActionRunner, IAction, IActionRunner, SubmenuAction, Separator } from 'vs/base/common/actions';
import { RunOnceScheduler } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import { KeyCode, ResolvedKeybinding, KeyMod } from 'vs/base/common/keyCodes';
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { withNullAsUndefined } from 'vs/base/common/types';
import { asArray } from 'vs/base/common/arrays';
import { ScanCodeUtils, ScanCode } from 'vs/base/common/scanCode';
import { isMacintosh } from 'vs/base/common/platform';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { Codicon, registerIcon } from 'vs/base/common/codicons';
const $ = DOM.$;
const menuBarMoreIcon = registerIcon('menubar-more', Codicon.more);
export interface IMenuBarOptions {
enableMnemonics?: boolean;
disableAltFocus?: boolean;
visibility?: string;
getKeybinding?: (action: IAction) => ResolvedKeybinding | undefined;
alwaysOnMnemonics?: boolean;
compactMode?: Direction;
getCompactMenuActions?: () => IAction[]
}
export interface MenuBarMenu {
actions: IAction[];
label: string;
}
enum MenubarState {
HIDDEN,
VISIBLE,
FOCUSED,
OPEN
}
export class MenuBar extends Disposable {
static readonly OVERFLOW_INDEX: number = -1;
private menuCache: {
buttonElement: HTMLElement;
titleElement: HTMLElement;
label: string;
actions?: IAction[];
}[];
private overflowMenu!: {
buttonElement: HTMLElement;
titleElement: HTMLElement;
label: string;
actions?: IAction[];
};
private focusedMenu: {
index: number;
holder?: HTMLElement;
widget?: Menu;
} | undefined;
private focusToReturn: HTMLElement | undefined;
private menuUpdater: RunOnceScheduler;
// Input-related
private _mnemonicsInUse: boolean = false;
private openedViaKeyboard: boolean = false;
private awaitingAltRelease: boolean = false;
private ignoreNextMouseUp: boolean = false;
private mnemonics: Map<string, number>;
private updatePending: boolean = false;
private _focusState: MenubarState;
private actionRunner: IActionRunner;
private readonly _onVisibilityChange: Emitter<boolean>;
private readonly _onFocusStateChange: Emitter<boolean>;
private numMenusShown: number = 0;
private menuStyle: IMenuStyles | undefined;
private overflowLayoutScheduled: IDisposable | undefined = undefined;
constructor(private container: HTMLElement, private options: IMenuBarOptions = {}) {
super();
this.container.setAttribute('role', 'menubar');
if (this.options.compactMode !== undefined) {
this.container.classList.add('compact');
}
this.menuCache = [];
this.mnemonics = new Map<string, number>();
this._focusState = MenubarState.VISIBLE;
this._onVisibilityChange = this._register(new Emitter<boolean>());
this._onFocusStateChange = this._register(new Emitter<boolean>());
this.createOverflowMenu();
this.menuUpdater = this._register(new RunOnceScheduler(() => this.update(), 200));
this.actionRunner = this._register(new ActionRunner());
this._register(this.actionRunner.onDidBeforeRun(() => {
this.setUnfocusedState();
}));
this._register(DOM.ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this));
this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
let eventHandled = true;
const key = !!e.key ? e.key.toLocaleLowerCase() : '';
if (event.equals(KeyCode.LeftArrow) || (isMacintosh && event.equals(KeyCode.Tab | KeyMod.Shift))) {
this.focusPrevious();
} else if (event.equals(KeyCode.RightArrow) || (isMacintosh && event.equals(KeyCode.Tab))) {
this.focusNext();
} else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) {
this.setUnfocusedState();
} else if (!this.isOpen && !event.ctrlKey && this.options.enableMnemonics && this.mnemonicsInUse && this.mnemonics.has(key)) {
const menuIndex = this.mnemonics.get(key)!;
this.onMenuTriggered(menuIndex, false);
} else {
eventHandled = false;
}
// Never allow default tab behavior when not compact
if (this.options.compactMode === undefined && (event.equals(KeyCode.Tab | KeyMod.Shift) || event.equals(KeyCode.Tab))) {
event.preventDefault();
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
}
}));
this._register(DOM.addDisposableListener(window, DOM.EventType.MOUSE_DOWN, () => {
// This mouse event is outside the menubar so it counts as a focus out
if (this.isFocused) {
this.setUnfocusedState();
}
}));
this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_IN, (e) => {
let event = e as FocusEvent;
if (event.relatedTarget) {
if (!this.container.contains(event.relatedTarget as HTMLElement)) {
this.focusToReturn = event.relatedTarget as HTMLElement;
}
}
}));
this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_OUT, (e) => {
let event = e as FocusEvent;
// We are losing focus and there is no related target, e.g. webview case
if (!event.relatedTarget) {
this.setUnfocusedState();
}
// We are losing focus and there is a target, reset focusToReturn value as not to redirect
else if (event.relatedTarget && !this.container.contains(event.relatedTarget as HTMLElement)) {
this.focusToReturn = undefined;
this.setUnfocusedState();
}
}));
this._register(DOM.addDisposableListener(window, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
if (!this.options.enableMnemonics || !e.altKey || e.ctrlKey || e.defaultPrevented) {
return;
}
const key = e.key.toLocaleLowerCase();
if (!this.mnemonics.has(key)) {
return;
}
this.mnemonicsInUse = true;
this.updateMnemonicVisibility(true);
const menuIndex = this.mnemonics.get(key)!;
this.onMenuTriggered(menuIndex, false);
}));
this.setUnfocusedState();
}
push(arg: MenuBarMenu | MenuBarMenu[]): void {
const menus: MenuBarMenu[] = asArray(arg);
menus.forEach((menuBarMenu) => {
const menuIndex = this.menuCache.length;
const cleanMenuLabel = cleanMnemonic(menuBarMenu.label);
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': cleanMenuLabel, 'aria-haspopup': true });
const titleElement = $('div.menubar-menu-title', { 'role': 'none', 'aria-hidden': true });
buttonElement.appendChild(titleElement);
this.container.insertBefore(buttonElement, this.overflowMenu.buttonElement);
let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(menuBarMenu.label);
// Register mnemonics
if (mnemonicMatches) {
let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[3];
this.registerMnemonic(this.menuCache.length, mnemonic);
}
this.updateLabels(titleElement, buttonElement, menuBarMenu.label);
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
let eventHandled = true;
if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) {
this.focusedMenu = { index: menuIndex };
this.openedViaKeyboard = true;
this.focusState = MenubarState.OPEN;
} else {
eventHandled = false;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
}
}));
this._register(Gesture.addTarget(buttonElement));
this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => {
// Ignore this touch if the menu is touched
if (this.isOpen && this.focusedMenu && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) {
return;
}
this.ignoreNextMouseUp = false;
this.onMenuTriggered(menuIndex, true);
e.preventDefault();
e.stopPropagation();
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
// Ignore non-left-click
const mouseEvent = new StandardMouseEvent(e);
if (!mouseEvent.leftButton) {
e.preventDefault();
return;
}
if (!this.isOpen) {
// Open the menu with mouse down and ignore the following mouse up event
this.ignoreNextMouseUp = true;
this.onMenuTriggered(menuIndex, true);
} else {
this.ignoreNextMouseUp = false;
}
e.preventDefault();
e.stopPropagation();
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => {
if (e.defaultPrevented) {
return;
}
if (!this.ignoreNextMouseUp) {
if (this.isFocused) {
this.onMenuTriggered(menuIndex, true);
}
} else {
this.ignoreNextMouseUp = false;
}
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => {
if (this.isOpen && !this.isCurrentMenu(menuIndex)) {
this.menuCache[menuIndex].buttonElement.focus();
this.cleanupCustomMenu();
this.showCustomMenu(menuIndex, false);
} else if (this.isFocused && !this.isOpen) {
this.focusedMenu = { index: menuIndex };
buttonElement.focus();
}
}));
this.menuCache.push({
label: menuBarMenu.label,
actions: menuBarMenu.actions,
buttonElement: buttonElement,
titleElement: titleElement
});
});
}
createOverflowMenu(): void {
const label = this.options.compactMode !== undefined ? nls.localize('mAppMenu', 'Application Menu') : nls.localize('mMore', 'More');
const title = this.options.compactMode !== undefined ? label : undefined;
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': this.options.compactMode !== undefined ? 0 : -1, 'aria-label': label, 'title': title, 'aria-haspopup': true });
const titleElement = $('div.menubar-menu-title.toolbar-toggle-more' + menuBarMoreIcon.cssSelector, { 'role': 'none', 'aria-hidden': true });
buttonElement.appendChild(titleElement);
this.container.appendChild(buttonElement);
buttonElement.style.visibility = 'hidden';
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
let eventHandled = true;
const triggerKeys = [KeyCode.Enter];
if (this.options.compactMode === undefined) {
triggerKeys.push(KeyCode.DownArrow);
} else {
triggerKeys.push(KeyCode.Space);
triggerKeys.push(this.options.compactMode === Direction.Right ? KeyCode.RightArrow : KeyCode.LeftArrow);
}
if ((triggerKeys.some(k => event.equals(k)) && !this.isOpen)) {
this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX };
this.openedViaKeyboard = true;
this.focusState = MenubarState.OPEN;
} else {
eventHandled = false;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
}
}));
this._register(Gesture.addTarget(buttonElement));
this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => {
// Ignore this touch if the menu is touched
if (this.isOpen && this.focusedMenu && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) {
return;
}
this.ignoreNextMouseUp = false;
this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);
e.preventDefault();
e.stopPropagation();
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e) => {
// Ignore non-left-click
const mouseEvent = new StandardMouseEvent(e);
if (!mouseEvent.leftButton) {
e.preventDefault();
return;
}
if (!this.isOpen) {
// Open the menu with mouse down and ignore the following mouse up event
this.ignoreNextMouseUp = true;
this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);
} else {
this.ignoreNextMouseUp = false;
}
e.preventDefault();
e.stopPropagation();
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => {
if (e.defaultPrevented) {
return;
}
if (!this.ignoreNextMouseUp) {
if (this.isFocused) {
this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);
}
} else {
this.ignoreNextMouseUp = false;
}
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => {
if (this.isOpen && !this.isCurrentMenu(MenuBar.OVERFLOW_INDEX)) {
this.overflowMenu.buttonElement.focus();
this.cleanupCustomMenu();
this.showCustomMenu(MenuBar.OVERFLOW_INDEX, false);
} else if (this.isFocused && !this.isOpen) {
this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX };
buttonElement.focus();
}
}));
this.overflowMenu = {
buttonElement: buttonElement,
titleElement: titleElement,
label: 'More'
};
}
updateMenu(menu: MenuBarMenu): void {
const menuToUpdate = this.menuCache.filter(menuBarMenu => menuBarMenu.label === menu.label);
if (menuToUpdate && menuToUpdate.length) {
menuToUpdate[0].actions = menu.actions;
}
}
dispose(): void {
super.dispose();
this.menuCache.forEach(menuBarMenu => {
menuBarMenu.titleElement.remove();
menuBarMenu.buttonElement.remove();
});
this.overflowMenu.titleElement.remove();
this.overflowMenu.buttonElement.remove();
dispose(this.overflowLayoutScheduled);
this.overflowLayoutScheduled = undefined;
}
blur(): void {
this.setUnfocusedState();
}
getWidth(): number {
if (this.menuCache) {
const left = this.menuCache[0].buttonElement.getBoundingClientRect().left;
const right = this.hasOverflow ? this.overflowMenu.buttonElement.getBoundingClientRect().right : this.menuCache[this.menuCache.length - 1].buttonElement.getBoundingClientRect().right;
return right - left;
}
return 0;
}
getHeight(): number {
return this.container.clientHeight;
}
toggleFocus(): void {
if (!this.isFocused && this.options.visibility !== 'hidden') {
this.mnemonicsInUse = true;
this.focusedMenu = { index: this.numMenusShown > 0 ? 0 : MenuBar.OVERFLOW_INDEX };
this.focusState = MenubarState.FOCUSED;
} else if (!this.isOpen) {
this.setUnfocusedState();
}
}
private updateOverflowAction(): void {
if (!this.menuCache || !this.menuCache.length) {
return;
}
const sizeAvailable = this.container.offsetWidth;
let currentSize = 0;
let full = this.options.compactMode !== undefined;
const prevNumMenusShown = this.numMenusShown;
this.numMenusShown = 0;
for (let menuBarMenu of this.menuCache) {
if (!full) {
const size = menuBarMenu.buttonElement.offsetWidth;
if (currentSize + size > sizeAvailable) {
full = true;
} else {
currentSize += size;
this.numMenusShown++;
if (this.numMenusShown > prevNumMenusShown) {
menuBarMenu.buttonElement.style.visibility = 'visible';
}
}
}
if (full) {
menuBarMenu.buttonElement.style.visibility = 'hidden';
}
}
// Overflow
if (full) {
// Can't fit the more button, need to remove more menus
while (currentSize + this.overflowMenu.buttonElement.offsetWidth > sizeAvailable && this.numMenusShown > 0) {
this.numMenusShown--;
const size = this.menuCache[this.numMenusShown].buttonElement.offsetWidth;
this.menuCache[this.numMenusShown].buttonElement.style.visibility = 'hidden';
currentSize -= size;
}
this.overflowMenu.actions = [];
for (let idx = this.numMenusShown; idx < this.menuCache.length; idx++) {
this.overflowMenu.actions.push(new SubmenuAction(`menubar.submenu.${this.menuCache[idx].label}`, this.menuCache[idx].label, this.menuCache[idx].actions || []));
}
if (this.overflowMenu.buttonElement.nextElementSibling !== this.menuCache[this.numMenusShown].buttonElement) {
this.overflowMenu.buttonElement.remove();
this.container.insertBefore(this.overflowMenu.buttonElement, this.menuCache[this.numMenusShown].buttonElement);
this.overflowMenu.buttonElement.style.visibility = 'visible';
}
const compactMenuActions = this.options.getCompactMenuActions?.();
if (compactMenuActions && compactMenuActions.length) {
this.overflowMenu.actions.push(new Separator());
this.overflowMenu.actions.push(...compactMenuActions);
}
} else {
this.overflowMenu.buttonElement.remove();
this.container.appendChild(this.overflowMenu.buttonElement);
this.overflowMenu.buttonElement.style.visibility = 'hidden';
}
}
private updateLabels(titleElement: HTMLElement, buttonElement: HTMLElement, label: string): void {
const cleanMenuLabel = cleanMnemonic(label);
// Update the button label to reflect mnemonics
if (this.options.enableMnemonics) {
let cleanLabel = strings.escape(label);
// This is global so reset it
MENU_ESCAPED_MNEMONIC_REGEX.lastIndex = 0;
let escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(cleanLabel);
// We can't use negative lookbehind so we match our negative and skip
while (escMatch && escMatch[1]) {
escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(cleanLabel);
}
const replaceDoubleEscapes = (str: string) => str.replace(/&amp;&amp;/g, '&amp;');
if (escMatch) {
titleElement.innerText = '';
titleElement.append(
strings.ltrim(replaceDoubleEscapes(cleanLabel.substr(0, escMatch.index)), ' '),
$('mnemonic', { 'aria-hidden': 'true' }, escMatch[3]),
strings.rtrim(replaceDoubleEscapes(cleanLabel.substr(escMatch.index + escMatch[0].length)), ' ')
);
} else {
titleElement.innerText = replaceDoubleEscapes(cleanLabel).trim();
}
} else {
titleElement.innerText = cleanMenuLabel.replace(/&&/g, '&');
}
let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(label);
// Register mnemonics
if (mnemonicMatches) {
let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[3];
if (this.options.enableMnemonics) {
buttonElement.setAttribute('aria-keyshortcuts', 'Alt+' + mnemonic.toLocaleLowerCase());
} else {
buttonElement.removeAttribute('aria-keyshortcuts');
}
}
}
style(style: IMenuStyles): void {
this.menuStyle = style;
}
update(options?: IMenuBarOptions): void {
if (options) {
this.options = options;
}
// Don't update while using the menu
if (this.isFocused) {
this.updatePending = true;
return;
}
this.menuCache.forEach(menuBarMenu => {
this.updateLabels(menuBarMenu.titleElement, menuBarMenu.buttonElement, menuBarMenu.label);
});
if (!this.overflowLayoutScheduled) {
this.overflowLayoutScheduled = DOM.scheduleAtNextAnimationFrame(() => {
this.updateOverflowAction();
this.overflowLayoutScheduled = undefined;
});
}
this.setUnfocusedState();
}
private registerMnemonic(menuIndex: number, mnemonic: string): void {
this.mnemonics.set(mnemonic.toLocaleLowerCase(), menuIndex);
}
private hideMenubar(): void {
if (this.container.style.display !== 'none') {
this.container.style.display = 'none';
this._onVisibilityChange.fire(false);
}
}
private showMenubar(): void {
if (this.container.style.display !== 'flex') {
this.container.style.display = 'flex';
this._onVisibilityChange.fire(true);
this.updateOverflowAction();
}
}
private get focusState(): MenubarState {
return this._focusState;
}
private set focusState(value: MenubarState) {
if (this._focusState >= MenubarState.FOCUSED && value < MenubarState.FOCUSED) {
// Losing focus, update the menu if needed
if (this.updatePending) {
this.menuUpdater.schedule();
this.updatePending = false;
}
}
if (value === this._focusState) {
return;
}
const isVisible = this.isVisible;
const isOpen = this.isOpen;
const isFocused = this.isFocused;
this._focusState = value;
switch (value) {
case MenubarState.HIDDEN:
if (isVisible) {
this.hideMenubar();
}
if (isOpen) {
this.cleanupCustomMenu();
}
if (isFocused) {
this.focusedMenu = undefined;
if (this.focusToReturn) {
this.focusToReturn.focus();
this.focusToReturn = undefined;
}
}
break;
case MenubarState.VISIBLE:
if (!isVisible) {
this.showMenubar();
}
if (isOpen) {
this.cleanupCustomMenu();
}
if (isFocused) {
if (this.focusedMenu) {
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.blur();
} else {
this.menuCache[this.focusedMenu.index].buttonElement.blur();
}
}
this.focusedMenu = undefined;
if (this.focusToReturn) {
this.focusToReturn.focus();
this.focusToReturn = undefined;
}
}
break;
case MenubarState.FOCUSED:
if (!isVisible) {
this.showMenubar();
}
if (isOpen) {
this.cleanupCustomMenu();
}
if (this.focusedMenu) {
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.focus();
} else {
this.menuCache[this.focusedMenu.index].buttonElement.focus();
}
}
break;
case MenubarState.OPEN:
if (!isVisible) {
this.showMenubar();
}
if (this.focusedMenu) {
this.showCustomMenu(this.focusedMenu.index, this.openedViaKeyboard);
}
break;
}
this._focusState = value;
this._onFocusStateChange.fire(this.focusState >= MenubarState.FOCUSED);
}
private get isVisible(): boolean {
return this.focusState >= MenubarState.VISIBLE;
}
private get isFocused(): boolean {
return this.focusState >= MenubarState.FOCUSED;
}
private get isOpen(): boolean {
return this.focusState >= MenubarState.OPEN;
}
private get hasOverflow(): boolean {
return this.numMenusShown < this.menuCache.length;
}
private setUnfocusedState(): void {
if (this.options.visibility === 'toggle' || this.options.visibility === 'hidden') {
this.focusState = MenubarState.HIDDEN;
} else if (this.options.visibility === 'default' && browser.isFullscreen()) {
this.focusState = MenubarState.HIDDEN;
} else {
this.focusState = MenubarState.VISIBLE;
}
this.ignoreNextMouseUp = false;
this.mnemonicsInUse = false;
this.updateMnemonicVisibility(false);
}
private focusPrevious(): void {
if (!this.focusedMenu || this.numMenusShown === 0) {
return;
}
let newFocusedIndex = (this.focusedMenu.index - 1 + this.numMenusShown) % this.numMenusShown;
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
newFocusedIndex = this.numMenusShown - 1;
} else if (this.focusedMenu.index === 0 && this.hasOverflow) {
newFocusedIndex = MenuBar.OVERFLOW_INDEX;
}
if (newFocusedIndex === this.focusedMenu.index) {
return;
}
if (this.isOpen) {
this.cleanupCustomMenu();
this.showCustomMenu(newFocusedIndex);
} else if (this.isFocused) {
this.focusedMenu.index = newFocusedIndex;
if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.focus();
} else {
this.menuCache[newFocusedIndex].buttonElement.focus();
}
}
}
private focusNext(): void {
if (!this.focusedMenu || this.numMenusShown === 0) {
return;
}
let newFocusedIndex = (this.focusedMenu.index + 1) % this.numMenusShown;
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
newFocusedIndex = 0;
} else if (this.focusedMenu.index === this.numMenusShown - 1) {
newFocusedIndex = MenuBar.OVERFLOW_INDEX;
}
if (newFocusedIndex === this.focusedMenu.index) {
return;
}
if (this.isOpen) {
this.cleanupCustomMenu();
this.showCustomMenu(newFocusedIndex);
} else if (this.isFocused) {
this.focusedMenu.index = newFocusedIndex;
if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.focus();
} else {
this.menuCache[newFocusedIndex].buttonElement.focus();
}
}
}
private updateMnemonicVisibility(visible: boolean): void {
if (this.menuCache) {
this.menuCache.forEach(menuBarMenu => {
if (menuBarMenu.titleElement.children.length) {
let child = menuBarMenu.titleElement.children.item(0) as HTMLElement;
if (child) {
child.style.textDecoration = (this.options.alwaysOnMnemonics || visible) ? 'underline' : '';
}
}
});
}
}
private get mnemonicsInUse(): boolean {
return this._mnemonicsInUse;
}
private set mnemonicsInUse(value: boolean) {
this._mnemonicsInUse = value;
}
public get onVisibilityChange(): Event<boolean> {
return this._onVisibilityChange.event;
}
public get onFocusStateChange(): Event<boolean> {
return this._onFocusStateChange.event;
}
private onMenuTriggered(menuIndex: number, clicked: boolean) {
if (this.isOpen) {
if (this.isCurrentMenu(menuIndex)) {
this.setUnfocusedState();
} else {
this.cleanupCustomMenu();
this.showCustomMenu(menuIndex, this.openedViaKeyboard);
}
} else {
this.focusedMenu = { index: menuIndex };
this.openedViaKeyboard = !clicked;
this.focusState = MenubarState.OPEN;
}
}
private onModifierKeyToggled(modifierKeyStatus: DOM.IModifierKeyStatus): void {
const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey && !modifierKeyStatus.metaKey;
if (this.options.visibility === 'hidden') {
return;
}
// Prevent alt-key default if the menu is not hidden and we use alt to focus
if (modifierKeyStatus.event && !this.options.disableAltFocus) {
if (ScanCodeUtils.toEnum(modifierKeyStatus.event.code) === ScanCode.AltLeft) {
modifierKeyStatus.event.preventDefault();
}
}
// Alt key pressed while menu is focused. This should return focus away from the menubar
if (this.isFocused && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.altKey) {
this.setUnfocusedState();
this.mnemonicsInUse = false;
this.awaitingAltRelease = true;
}
// Clean alt key press and release
if (allModifiersReleased && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.lastKeyReleased === 'alt') {
if (!this.awaitingAltRelease) {
if (!this.isFocused && !(this.options.disableAltFocus && this.options.visibility !== 'toggle')) {
this.mnemonicsInUse = true;
this.focusedMenu = { index: this.numMenusShown > 0 ? 0 : MenuBar.OVERFLOW_INDEX };
this.focusState = MenubarState.FOCUSED;
} else if (!this.isOpen) {
this.setUnfocusedState();
}
}
}
// Alt key released
if (!modifierKeyStatus.altKey && modifierKeyStatus.lastKeyReleased === 'alt') {
this.awaitingAltRelease = false;
}
if (this.options.enableMnemonics && this.menuCache && !this.isOpen) {
this.updateMnemonicVisibility((!this.awaitingAltRelease && modifierKeyStatus.altKey) || this.mnemonicsInUse);
}
}
private isCurrentMenu(menuIndex: number): boolean {
if (!this.focusedMenu) {
return false;
}
return this.focusedMenu.index === menuIndex;
}
private cleanupCustomMenu(): void {
if (this.focusedMenu) {
// Remove focus from the menus first
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.focus();
} else {
this.menuCache[this.focusedMenu.index].buttonElement.focus();
}
if (this.focusedMenu.holder) {
if (this.focusedMenu.holder.parentElement) {
this.focusedMenu.holder.parentElement.classList.remove('open');
}
this.focusedMenu.holder.remove();
}
if (this.focusedMenu.widget) {
this.focusedMenu.widget.dispose();
}
this.focusedMenu = { index: this.focusedMenu.index };
}
}
private showCustomMenu(menuIndex: number, selectFirst = true): void {
const actualMenuIndex = menuIndex >= this.numMenusShown ? MenuBar.OVERFLOW_INDEX : menuIndex;
const customMenu = actualMenuIndex === MenuBar.OVERFLOW_INDEX ? this.overflowMenu : this.menuCache[actualMenuIndex];
if (!customMenu.actions) {
return;
}
const menuHolder = $('div.menubar-menu-items-holder', { 'title': '' });
customMenu.buttonElement.classList.add('open');
const buttonBoundingRect = customMenu.buttonElement.getBoundingClientRect();
if (this.options.compactMode === Direction.Right) {
menuHolder.style.top = `${buttonBoundingRect.top}px`;
menuHolder.style.left = `${buttonBoundingRect.left + this.container.clientWidth}px`;
} else if (this.options.compactMode === Direction.Left) {
menuHolder.style.top = `${buttonBoundingRect.top}px`;
menuHolder.style.right = `${this.container.clientWidth}px`;
menuHolder.style.left = 'auto';
} else {
menuHolder.style.top = `${this.container.clientHeight}px`;
menuHolder.style.left = `${buttonBoundingRect.left}px`;
}
customMenu.buttonElement.appendChild(menuHolder);
let menuOptions: IMenuOptions = {
getKeyBinding: this.options.getKeybinding,
actionRunner: this.actionRunner,
enableMnemonics: this.options.alwaysOnMnemonics || (this.mnemonicsInUse && this.options.enableMnemonics),
ariaLabel: withNullAsUndefined(customMenu.buttonElement.getAttribute('aria-label')),
expandDirection: this.options.compactMode !== undefined ? this.options.compactMode : Direction.Right,
useEventAsContext: true
};
let menuWidget = this._register(new Menu(menuHolder, customMenu.actions, menuOptions));
if (this.menuStyle) {
menuWidget.style(this.menuStyle);
}
this._register(menuWidget.onDidCancel(() => {
this.focusState = MenubarState.FOCUSED;
}));
if (actualMenuIndex !== menuIndex) {
menuWidget.trigger(menuIndex - this.numMenusShown);
} else {
menuWidget.focus(selectFirst);
}
this.focusedMenu = {
index: actualMenuIndex,
holder: menuHolder,
widget: menuWidget
};
}
}

View File

@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-mouse-cursor-text {
cursor: text;
}
/* The following selector looks a bit funny, but that is needed to cover all the workbench and the editor!! */
.vs-dark .mac .monaco-mouse-cursor-text, .hc-black .mac .monaco-mouse-cursor-text,
.vs-dark.mac .monaco-mouse-cursor-text, .hc-black.mac .monaco-mouse-cursor-text {
cursor: -webkit-image-set(url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAL0lEQVQoz2NgCD3x//9/BhBYBWdhgFVAiVW4JBFKGIa4AqD0//9D3pt4I4tAdAMAHTQ/j5Zom30AAAAASUVORK5CYII=') 1x, url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVRIx2NgYGBY/R8I/vx5eelX3n82IJ9FxGf6tksvf/8FiTMQAcAGQMDvSwu09abffY8QYSAScNk45G198eX//yev73/4///701eh//kZSARckrNBRvz//+8+6ZohwCzjGNjdgQxkAg7B9WADeBjIBqtJCbhRA0YNoIkBSNmaPEMoNmA0FkYNoFKhapJ6FGyAH3nauaSmPfwI0v/3OukVi0CIZ+F25KrtYcx/CTIy0e+rC7R1Z4KMICVTQQ14feVXIbR695u14+Ir4gwAAD49E54wc1kWAAAAAElFTkSuQmCC') 2x) 5 8, text;
}

View File

@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./mouseCursor';
export const MOUSE_CURSOR_TEXT_CSS_CLASS_NAME = `monaco-mouse-cursor-text`;

View File

@ -0,0 +1,48 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-progress-container {
width: 100%;
height: 5px;
overflow: hidden; /* keep progress bit in bounds */
}
.monaco-progress-container .progress-bit {
width: 2%;
height: 5px;
position: absolute;
left: 0;
display: none;
}
.monaco-progress-container.active .progress-bit {
display: inherit;
}
.monaco-progress-container.discrete .progress-bit {
left: 0;
transition: width 100ms linear;
}
.monaco-progress-container.discrete.done .progress-bit {
width: 100%;
}
.monaco-progress-container.infinite .progress-bit {
animation-name: progress;
animation-duration: 4s;
animation-iteration-count: infinite;
animation-timing-function: linear;
transform: translate3d(0px, 0px, 0px);
}
/**
* The progress bit has a width: 2% (1/50) of the parent container. The animation moves it from 0% to 100% of
* that container. Since translateX is relative to the progress bit size, we have to multiple it with
* its relative size to the parent container:
* 50%: 50 * 50 = 2500%
* 100%: 50 * 100 - 50 (do not overflow): 4950%
*/
@keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4950%) scaleX(1) } }

View File

@ -0,0 +1,217 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./progressbar';
import { Disposable } from 'vs/base/common/lifecycle';
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
import { hide, show } from 'vs/base/browser/dom';
import { RunOnceScheduler } from 'vs/base/common/async';
import { isNumber } from 'vs/base/common/types';
const CSS_DONE = 'done';
const CSS_ACTIVE = 'active';
const CSS_INFINITE = 'infinite';
const CSS_DISCRETE = 'discrete';
export interface IProgressBarOptions extends IProgressBarStyles {
}
export interface IProgressBarStyles {
progressBarBackground?: Color;
}
const defaultOpts = {
progressBarBackground: Color.fromHex('#0E70C0')
};
/**
* A progress bar with support for infinite or discrete progress.
*/
export class ProgressBar extends Disposable {
private options: IProgressBarOptions;
private workedVal: number;
private element!: HTMLElement;
private bit!: HTMLElement;
private totalWork: number | undefined;
private progressBarBackground: Color | undefined;
private showDelayedScheduler: RunOnceScheduler;
constructor(container: HTMLElement, options?: IProgressBarOptions) {
super();
this.options = options || Object.create(null);
mixin(this.options, defaultOpts, false);
this.workedVal = 0;
this.progressBarBackground = this.options.progressBarBackground;
this._register(this.showDelayedScheduler = new RunOnceScheduler(() => show(this.element), 0));
this.create(container);
}
private create(container: HTMLElement): void {
this.element = document.createElement('div');
this.element.classList.add('monaco-progress-container');
this.element.setAttribute('role', 'progressbar');
container.appendChild(this.element);
this.bit = document.createElement('div');
this.bit.classList.add('progress-bit');
this.element.appendChild(this.bit);
this.applyStyles();
}
private off(): void {
this.bit.style.width = 'inherit';
this.bit.style.opacity = '1';
this.element.classList.remove(CSS_ACTIVE, CSS_INFINITE, CSS_DISCRETE);
this.workedVal = 0;
this.totalWork = undefined;
}
/**
* Indicates to the progress bar that all work is done.
*/
done(): ProgressBar {
return this.doDone(true);
}
/**
* Stops the progressbar from showing any progress instantly without fading out.
*/
stop(): ProgressBar {
return this.doDone(false);
}
private doDone(delayed: boolean): ProgressBar {
this.element.classList.add(CSS_DONE);
// let it grow to 100% width and hide afterwards
if (!this.element.classList.contains(CSS_INFINITE)) {
this.bit.style.width = 'inherit';
if (delayed) {
setTimeout(() => this.off(), 200);
} else {
this.off();
}
}
// let it fade out and hide afterwards
else {
this.bit.style.opacity = '0';
if (delayed) {
setTimeout(() => this.off(), 200);
} else {
this.off();
}
}
return this;
}
/**
* Use this mode to indicate progress that has no total number of work units.
*/
infinite(): ProgressBar {
this.bit.style.width = '2%';
this.bit.style.opacity = '1';
this.element.classList.remove(CSS_DISCRETE, CSS_DONE);
this.element.classList.add(CSS_ACTIVE, CSS_INFINITE);
return this;
}
/**
* Tells the progress bar the total number of work. Use in combination with workedVal() to let
* the progress bar show the actual progress based on the work that is done.
*/
total(value: number): ProgressBar {
this.workedVal = 0;
this.totalWork = value;
this.element.setAttribute('aria-valuemax', value.toString());
return this;
}
/**
* Finds out if this progress bar is configured with total work
*/
hasTotal(): boolean {
return isNumber(this.totalWork);
}
/**
* Tells the progress bar that an increment of work has been completed.
*/
worked(value: number): ProgressBar {
value = Math.max(1, Number(value));
return this.doSetWorked(this.workedVal + value);
}
/**
* Tells the progress bar the total amount of work that has been completed.
*/
setWorked(value: number): ProgressBar {
value = Math.max(1, Number(value));
return this.doSetWorked(value);
}
private doSetWorked(value: number): ProgressBar {
const totalWork = this.totalWork || 100;
this.workedVal = value;
this.workedVal = Math.min(totalWork, this.workedVal);
this.element.classList.remove(CSS_INFINITE, CSS_DONE);
this.element.classList.add(CSS_ACTIVE, CSS_DISCRETE);
this.element.setAttribute('aria-valuenow', value.toString());
this.bit.style.width = 100 * (this.workedVal / (totalWork)) + '%';
return this;
}
getContainer(): HTMLElement {
return this.element;
}
show(delay?: number): void {
this.showDelayedScheduler.cancel();
if (typeof delay === 'number') {
this.showDelayedScheduler.schedule(delay);
} else {
show(this.element);
}
}
hide(): void {
hide(this.element);
this.showDelayedScheduler.cancel();
}
style(styles: IProgressBarStyles): void {
this.progressBarBackground = styles.progressBarBackground;
this.applyStyles();
}
protected applyStyles(): void {
if (this.bit) {
const background = this.progressBarBackground ? this.progressBarBackground.toString() : '';
this.bit.style.backgroundColor = background;
}
}
}

View File

@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
:root {
--sash-size: 4px;
}
.monaco-sash {
position: absolute;
z-index: 35;
touch-action: none;
}
.monaco-sash.disabled {
pointer-events: none;
}
.monaco-sash.mac.vertical {
cursor: col-resize;
}
.monaco-sash.vertical.minimum {
cursor: e-resize;
}
.monaco-sash.vertical.maximum {
cursor: w-resize;
}
.monaco-sash.mac.horizontal {
cursor: row-resize;
}
.monaco-sash.horizontal.minimum {
cursor: s-resize;
}
.monaco-sash.horizontal.maximum {
cursor: n-resize;
}
.monaco-sash.disabled {
cursor: default !important;
pointer-events: none !important;
}
.monaco-sash.vertical {
cursor: ew-resize;
top: 0;
width: var(--sash-size);
height: 100%;
}
.monaco-sash.horizontal {
cursor: ns-resize;
left: 0;
width: 100%;
height: var(--sash-size);
}
.monaco-sash:not(.disabled).orthogonal-start::before, .monaco-sash:not(.disabled).orthogonal-end::after {
content: ' ';
height: calc(var(--sash-size) * 2);
width: calc(var(--sash-size) * 2);
z-index: 100;
display: block;
cursor: all-scroll; position: absolute;
}
.monaco-sash.orthogonal-start.vertical::before { left: -calc(var(--sash-size) / 2); top: calc(var(--sash-size) * -1); }
.monaco-sash.orthogonal-end.vertical::after { left: -calc(var(--sash-size) / 2); bottom: calc(var(--sash-size) * -1); }
.monaco-sash.orthogonal-start.horizontal::before { top: -calc(var(--sash-size) / 2); left: calc(var(--sash-size) * -1); }
.monaco-sash.orthogonal-end.horizontal::after { top: -calc(var(--sash-size) / 2); right: calc(var(--sash-size) * -1); }
/** Debug **/
.monaco-sash.debug {
background: cyan;
}
.monaco-sash.debug.disabled {
background: rgba(0, 255, 255, 0.2);
}
.monaco-sash.debug:not(.disabled).orthogonal-start::before,
.monaco-sash.debug:not(.disabled).orthogonal-end::after {
background: red;
}

View File

@ -0,0 +1,428 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./sash';
import { IDisposable, dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { isMacintosh } from 'vs/base/common/platform';
import * as types from 'vs/base/common/types';
import { EventType, GestureEvent, Gesture } from 'vs/base/browser/touch';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { Event, Emitter } from 'vs/base/common/event';
import { getElementsByTagName, EventHelper, createStyleSheet, addDisposableListener, append, $ } from 'vs/base/browser/dom';
import { domEvent } from 'vs/base/browser/event';
const DEBUG = false;
export interface ISashLayoutProvider { }
export interface IVerticalSashLayoutProvider extends ISashLayoutProvider {
getVerticalSashLeft(sash: Sash): number;
getVerticalSashTop?(sash: Sash): number;
getVerticalSashHeight?(sash: Sash): number;
}
export interface IHorizontalSashLayoutProvider extends ISashLayoutProvider {
getHorizontalSashTop(sash: Sash): number;
getHorizontalSashLeft?(sash: Sash): number;
getHorizontalSashWidth?(sash: Sash): number;
}
export interface ISashEvent {
startX: number;
currentX: number;
startY: number;
currentY: number;
altKey: boolean;
}
export interface ISashOptions {
readonly orientation: Orientation;
readonly orthogonalStartSash?: Sash;
readonly orthogonalEndSash?: Sash;
readonly size?: number;
}
export interface IVerticalSashOptions extends ISashOptions {
readonly orientation: Orientation.VERTICAL;
}
export interface IHorizontalSashOptions extends ISashOptions {
readonly orientation: Orientation.HORIZONTAL;
}
export const enum Orientation {
VERTICAL,
HORIZONTAL
}
export const enum SashState {
Disabled,
Minimum,
Maximum,
Enabled
}
let globalSize = 4;
const onDidChangeGlobalSize = new Emitter<number>();
export function setGlobalSashSize(size: number): void {
globalSize = size;
onDidChangeGlobalSize.fire(size);
}
export class Sash extends Disposable {
private el: HTMLElement;
private layoutProvider: ISashLayoutProvider;
private hidden: boolean;
private orientation!: Orientation;
private size: number;
private _state: SashState = SashState.Enabled;
get state(): SashState { return this._state; }
set state(state: SashState) {
if (this._state === state) {
return;
}
this.el.classList.toggle('disabled', state === SashState.Disabled);
this.el.classList.toggle('minimum', state === SashState.Minimum);
this.el.classList.toggle('maximum', state === SashState.Maximum);
this._state = state;
this._onDidEnablementChange.fire(state);
}
private readonly _onDidEnablementChange = this._register(new Emitter<SashState>());
readonly onDidEnablementChange: Event<SashState> = this._onDidEnablementChange.event;
private readonly _onDidStart = this._register(new Emitter<ISashEvent>());
readonly onDidStart: Event<ISashEvent> = this._onDidStart.event;
private readonly _onDidChange = this._register(new Emitter<ISashEvent>());
readonly onDidChange: Event<ISashEvent> = this._onDidChange.event;
private readonly _onDidReset = this._register(new Emitter<void>());
readonly onDidReset: Event<void> = this._onDidReset.event;
private readonly _onDidEnd = this._register(new Emitter<void>());
readonly onDidEnd: Event<void> = this._onDidEnd.event;
linkedSash: Sash | undefined = undefined;
private readonly orthogonalStartSashDisposables = this._register(new DisposableStore());
private _orthogonalStartSash: Sash | undefined;
get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; }
set orthogonalStartSash(sash: Sash | undefined) {
this.orthogonalStartSashDisposables.clear();
if (sash) {
this.orthogonalStartSashDisposables.add(sash.onDidEnablementChange(this.onOrthogonalStartSashEnablementChange, this));
this.onOrthogonalStartSashEnablementChange(sash.state);
} else {
this.onOrthogonalStartSashEnablementChange(SashState.Disabled);
}
this._orthogonalStartSash = sash;
}
private readonly orthogonalEndSashDisposables = this._register(new DisposableStore());
private _orthogonalEndSash: Sash | undefined;
get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; }
set orthogonalEndSash(sash: Sash | undefined) {
this.orthogonalEndSashDisposables.clear();
if (sash) {
this.orthogonalEndSashDisposables.add(sash.onDidEnablementChange(this.onOrthogonalEndSashEnablementChange, this));
this.onOrthogonalEndSashEnablementChange(sash.state);
} else {
this.onOrthogonalEndSashEnablementChange(SashState.Disabled);
}
this._orthogonalEndSash = sash;
}
constructor(container: HTMLElement, layoutProvider: IVerticalSashLayoutProvider, options: ISashOptions);
constructor(container: HTMLElement, layoutProvider: IHorizontalSashLayoutProvider, options: ISashOptions);
constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions) {
super();
this.el = append(container, $('.monaco-sash'));
if (isMacintosh) {
this.el.classList.add('mac');
}
this._register(domEvent(this.el, 'mousedown')(this.onMouseDown, this));
this._register(domEvent(this.el, 'dblclick')(this.onMouseDoubleClick, this));
this._register(Gesture.addTarget(this.el));
this._register(domEvent(this.el, EventType.Start)(this.onTouchStart, this));
if (typeof options.size === 'number') {
this.size = options.size;
if (options.orientation === Orientation.VERTICAL) {
this.el.style.width = `${this.size}px`;
} else {
this.el.style.height = `${this.size}px`;
}
} else {
this.size = globalSize;
this._register(onDidChangeGlobalSize.event(size => {
this.size = size;
this.layout();
}));
}
this.hidden = false;
this.layoutProvider = layoutProvider;
this.orthogonalStartSash = options.orthogonalStartSash;
this.orthogonalEndSash = options.orthogonalEndSash;
this.orientation = options.orientation || Orientation.VERTICAL;
if (this.orientation === Orientation.HORIZONTAL) {
this.el.classList.add('horizontal');
this.el.classList.remove('vertical');
} else {
this.el.classList.remove('horizontal');
this.el.classList.add('vertical');
}
this.el.classList.toggle('debug', DEBUG);
this.layout();
}
private onMouseDown(e: MouseEvent): void {
EventHelper.stop(e, false);
let isMultisashResize = false;
if (!(e as any).__orthogonalSashEvent) {
const orthogonalSash = this.getOrthogonalSash(e);
if (orthogonalSash) {
isMultisashResize = true;
(e as any).__orthogonalSashEvent = true;
orthogonalSash.onMouseDown(e);
}
}
if (this.linkedSash && !(e as any).__linkedSashEvent) {
(e as any).__linkedSashEvent = true;
this.linkedSash.onMouseDown(e);
}
if (!this.state) {
return;
}
// Select both iframes and webviews; internally Electron nests an iframe
// in its <webview> component, but this isn't queryable.
const iframes = [
...getElementsByTagName('iframe'),
...getElementsByTagName('webview'),
];
for (const iframe of iframes) {
iframe.style.pointerEvents = 'none'; // disable mouse events on iframes as long as we drag the sash
}
const mouseDownEvent = new StandardMouseEvent(e);
const startX = mouseDownEvent.posx;
const startY = mouseDownEvent.posy;
const altKey = mouseDownEvent.altKey;
const startEvent: ISashEvent = { startX, currentX: startX, startY, currentY: startY, altKey };
this.el.classList.add('active');
this._onDidStart.fire(startEvent);
// fix https://github.com/microsoft/vscode/issues/21675
const style = createStyleSheet(this.el);
const updateStyle = () => {
let cursor = '';
if (isMultisashResize) {
cursor = 'all-scroll';
} else if (this.orientation === Orientation.HORIZONTAL) {
if (this.state === SashState.Minimum) {
cursor = 's-resize';
} else if (this.state === SashState.Maximum) {
cursor = 'n-resize';
} else {
cursor = isMacintosh ? 'row-resize' : 'ns-resize';
}
} else {
if (this.state === SashState.Minimum) {
cursor = 'e-resize';
} else if (this.state === SashState.Maximum) {
cursor = 'w-resize';
} else {
cursor = isMacintosh ? 'col-resize' : 'ew-resize';
}
}
style.textContent = `* { cursor: ${cursor} !important; }`;
};
const disposables = new DisposableStore();
updateStyle();
if (!isMultisashResize) {
this.onDidEnablementChange(updateStyle, null, disposables);
}
const onMouseMove = (e: MouseEvent) => {
EventHelper.stop(e, false);
const mouseMoveEvent = new StandardMouseEvent(e);
const event: ISashEvent = { startX, currentX: mouseMoveEvent.posx, startY, currentY: mouseMoveEvent.posy, altKey };
this._onDidChange.fire(event);
};
const onMouseUp = (e: MouseEvent) => {
EventHelper.stop(e, false);
this.el.removeChild(style);
this.el.classList.remove('active');
this._onDidEnd.fire();
disposables.dispose();
for (const iframe of iframes) {
iframe.style.pointerEvents = 'auto';
}
};
domEvent(window, 'mousemove')(onMouseMove, null, disposables);
domEvent(window, 'mouseup')(onMouseUp, null, disposables);
}
private onMouseDoubleClick(e: MouseEvent): void {
const orthogonalSash = this.getOrthogonalSash(e);
if (orthogonalSash) {
orthogonalSash._onDidReset.fire();
}
if (this.linkedSash) {
this.linkedSash._onDidReset.fire();
}
this._onDidReset.fire();
}
private onTouchStart(event: GestureEvent): void {
EventHelper.stop(event);
const listeners: IDisposable[] = [];
const startX = event.pageX;
const startY = event.pageY;
const altKey = event.altKey;
this._onDidStart.fire({
startX: startX,
currentX: startX,
startY: startY,
currentY: startY,
altKey
});
listeners.push(addDisposableListener(this.el, EventType.Change, (event: GestureEvent) => {
if (types.isNumber(event.pageX) && types.isNumber(event.pageY)) {
this._onDidChange.fire({
startX: startX,
currentX: event.pageX,
startY: startY,
currentY: event.pageY,
altKey
});
}
}));
listeners.push(addDisposableListener(this.el, EventType.End, (event: GestureEvent) => {
this._onDidEnd.fire();
dispose(listeners);
}));
}
layout(): void {
if (this.orientation === Orientation.VERTICAL) {
const verticalProvider = (<IVerticalSashLayoutProvider>this.layoutProvider);
this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (this.size / 2) + 'px';
if (verticalProvider.getVerticalSashTop) {
this.el.style.top = verticalProvider.getVerticalSashTop(this) + 'px';
}
if (verticalProvider.getVerticalSashHeight) {
this.el.style.height = verticalProvider.getVerticalSashHeight(this) + 'px';
}
} else {
const horizontalProvider = (<IHorizontalSashLayoutProvider>this.layoutProvider);
this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (this.size / 2) + 'px';
if (horizontalProvider.getHorizontalSashLeft) {
this.el.style.left = horizontalProvider.getHorizontalSashLeft(this) + 'px';
}
if (horizontalProvider.getHorizontalSashWidth) {
this.el.style.width = horizontalProvider.getHorizontalSashWidth(this) + 'px';
}
}
}
show(): void {
this.hidden = false;
this.el.style.removeProperty('display');
this.el.setAttribute('aria-hidden', 'false');
}
hide(): void {
this.hidden = true;
this.el.style.display = 'none';
this.el.setAttribute('aria-hidden', 'true');
}
isHidden(): boolean {
return this.hidden;
}
private onOrthogonalStartSashEnablementChange(state: SashState): void {
this.el.classList.toggle('orthogonal-start', state !== SashState.Disabled);
}
private onOrthogonalEndSashEnablementChange(state: SashState): void {
this.el.classList.toggle('orthogonal-end', state !== SashState.Disabled);
}
private getOrthogonalSash(e: MouseEvent): Sash | undefined {
if (this.orientation === Orientation.VERTICAL) {
if (e.offsetY <= this.size) {
return this.orthogonalStartSash;
} else if (e.offsetY >= this.el.clientHeight - this.size) {
return this.orthogonalEndSash;
}
} else {
if (e.offsetX <= this.size) {
return this.orthogonalStartSash;
} else if (e.offsetX >= this.el.clientWidth - this.size) {
return this.orthogonalEndSash;
}
}
return undefined;
}
dispose(): void {
super.dispose();
this.el.remove();
}
}

View File

@ -0,0 +1,282 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
import { IMouseEvent, StandardWheelEvent } from 'vs/base/browser/mouseEvent';
import { ScrollbarArrow, ScrollbarArrowOptions } from 'vs/base/browser/ui/scrollbar/scrollbarArrow';
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
import { ScrollbarVisibilityController } from 'vs/base/browser/ui/scrollbar/scrollbarVisibilityController';
import { Widget } from 'vs/base/browser/ui/widget';
import * as platform from 'vs/base/common/platform';
import { INewScrollPosition, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
/**
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
*/
const MOUSE_DRAG_RESET_DISTANCE = 140;
export interface ISimplifiedMouseEvent {
buttons: number;
posx: number;
posy: number;
}
export interface ScrollbarHost {
onMouseWheel(mouseWheelEvent: StandardWheelEvent): void;
onDragStart(): void;
onDragEnd(): void;
}
export interface AbstractScrollbarOptions {
lazyRender: boolean;
host: ScrollbarHost;
scrollbarState: ScrollbarState;
visibility: ScrollbarVisibility;
extraScrollbarClassName: string;
scrollable: Scrollable;
}
export abstract class AbstractScrollbar extends Widget {
protected _host: ScrollbarHost;
protected _scrollable: Scrollable;
private _lazyRender: boolean;
protected _scrollbarState: ScrollbarState;
private _visibilityController: ScrollbarVisibilityController;
private _mouseMoveMonitor: GlobalMouseMoveMonitor<IStandardMouseMoveEventData>;
public domNode: FastDomNode<HTMLElement>;
public slider!: FastDomNode<HTMLElement>;
protected _shouldRender: boolean;
constructor(opts: AbstractScrollbarOptions) {
super();
this._lazyRender = opts.lazyRender;
this._host = opts.host;
this._scrollable = opts.scrollable;
this._scrollbarState = opts.scrollbarState;
this._visibilityController = this._register(new ScrollbarVisibilityController(opts.visibility, 'visible scrollbar ' + opts.extraScrollbarClassName, 'invisible scrollbar ' + opts.extraScrollbarClassName));
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._mouseMoveMonitor = this._register(new GlobalMouseMoveMonitor<IStandardMouseMoveEventData>());
this._shouldRender = true;
this.domNode = createFastDomNode(document.createElement('div'));
this.domNode.setAttribute('role', 'presentation');
this.domNode.setAttribute('aria-hidden', 'true');
this._visibilityController.setDomNode(this.domNode);
this.domNode.setPosition('absolute');
this.onmousedown(this.domNode.domNode, (e) => this._domNodeMouseDown(e));
}
// ----------------- creation
/**
* Creates the dom node for an arrow & adds it to the container
*/
protected _createArrow(opts: ScrollbarArrowOptions): void {
let arrow = this._register(new ScrollbarArrow(opts));
this.domNode.domNode.appendChild(arrow.bgDomNode);
this.domNode.domNode.appendChild(arrow.domNode);
}
/**
* Creates the slider dom node, adds it to the container & hooks up the events
*/
protected _createSlider(top: number, left: number, width: number | undefined, height: number | undefined): void {
this.slider = createFastDomNode(document.createElement('div'));
this.slider.setClassName('slider');
this.slider.setPosition('absolute');
this.slider.setTop(top);
this.slider.setLeft(left);
if (typeof width === 'number') {
this.slider.setWidth(width);
}
if (typeof height === 'number') {
this.slider.setHeight(height);
}
this.slider.setLayerHinting(true);
this.slider.setContain('strict');
this.domNode.domNode.appendChild(this.slider.domNode);
this.onmousedown(this.slider.domNode, (e) => {
if (e.leftButton) {
e.preventDefault();
this._sliderMouseDown(e, () => { /*nothing to do*/ });
}
});
this.onclick(this.slider.domNode, e => {
if (e.leftButton) {
e.stopPropagation();
}
});
}
// ----------------- Update state
protected _onElementSize(visibleSize: number): boolean {
if (this._scrollbarState.setVisibleSize(visibleSize)) {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
return this._shouldRender;
}
protected _onElementScrollSize(elementScrollSize: number): boolean {
if (this._scrollbarState.setScrollSize(elementScrollSize)) {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
return this._shouldRender;
}
protected _onElementScrollPosition(elementScrollPosition: number): boolean {
if (this._scrollbarState.setScrollPosition(elementScrollPosition)) {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
return this._shouldRender;
}
// ----------------- rendering
public beginReveal(): void {
this._visibilityController.setShouldBeVisible(true);
}
public beginHide(): void {
this._visibilityController.setShouldBeVisible(false);
}
public render(): void {
if (!this._shouldRender) {
return;
}
this._shouldRender = false;
this._renderDomNode(this._scrollbarState.getRectangleLargeSize(), this._scrollbarState.getRectangleSmallSize());
this._updateSlider(this._scrollbarState.getSliderSize(), this._scrollbarState.getArrowSize() + this._scrollbarState.getSliderPosition());
}
// ----------------- DOM events
private _domNodeMouseDown(e: IMouseEvent): void {
if (e.target !== this.domNode.domNode) {
return;
}
this._onMouseDown(e);
}
public delegateMouseDown(e: IMouseEvent): void {
let domTop = this.domNode.domNode.getClientRects()[0].top;
let sliderStart = domTop + this._scrollbarState.getSliderPosition();
let sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
let mousePos = this._sliderMousePosition(e);
if (sliderStart <= mousePos && mousePos <= sliderStop) {
// Act as if it was a mouse down on the slider
if (e.leftButton) {
e.preventDefault();
this._sliderMouseDown(e, () => { /*nothing to do*/ });
}
} else {
// Act as if it was a mouse down on the scrollbar
this._onMouseDown(e);
}
}
private _onMouseDown(e: IMouseEvent): void {
let offsetX: number;
let offsetY: number;
if (e.target === this.domNode.domNode && typeof e.browserEvent.offsetX === 'number' && typeof e.browserEvent.offsetY === 'number') {
offsetX = e.browserEvent.offsetX;
offsetY = e.browserEvent.offsetY;
} else {
const domNodePosition = dom.getDomNodePagePosition(this.domNode.domNode);
offsetX = e.posx - domNodePosition.left;
offsetY = e.posy - domNodePosition.top;
}
this._setDesiredScrollPositionNow(this._scrollbarState.getDesiredScrollPositionFromOffset(this._mouseDownRelativePosition(offsetX, offsetY)));
if (e.leftButton) {
e.preventDefault();
this._sliderMouseDown(e, () => { /*nothing to do*/ });
}
}
private _sliderMouseDown(e: IMouseEvent, onDragFinished: () => void): void {
const initialMousePosition = this._sliderMousePosition(e);
const initialMouseOrthogonalPosition = this._sliderOrthogonalMousePosition(e);
const initialScrollbarState = this._scrollbarState.clone();
this.slider.toggleClassName('active', true);
this._mouseMoveMonitor.startMonitoring(
e.target,
e.buttons,
standardMouseMoveMerger,
(mouseMoveData: IStandardMouseMoveEventData) => {
const mouseOrthogonalPosition = this._sliderOrthogonalMousePosition(mouseMoveData);
const mouseOrthogonalDelta = Math.abs(mouseOrthogonalPosition - initialMouseOrthogonalPosition);
if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) {
// The mouse has wondered away from the scrollbar => reset dragging
this._setDesiredScrollPositionNow(initialScrollbarState.getScrollPosition());
return;
}
const mousePosition = this._sliderMousePosition(mouseMoveData);
const mouseDelta = mousePosition - initialMousePosition;
this._setDesiredScrollPositionNow(initialScrollbarState.getDesiredScrollPositionFromDelta(mouseDelta));
},
() => {
this.slider.toggleClassName('active', false);
this._host.onDragEnd();
onDragFinished();
}
);
this._host.onDragStart();
}
private _setDesiredScrollPositionNow(_desiredScrollPosition: number): void {
let desiredScrollPosition: INewScrollPosition = {};
this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition);
this._scrollable.setScrollPositionNow(desiredScrollPosition);
}
public updateScrollbarSize(scrollbarSize: number): void {
this._updateScrollbarSize(scrollbarSize);
this._scrollbarState.setScrollbarSize(scrollbarSize);
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
// ----------------- Overwrite these
protected abstract _renderDomNode(largeSize: number, smallSize: number): void;
protected abstract _updateSlider(sliderSize: number, sliderPosition: number): void;
protected abstract _mouseDownRelativePosition(offsetX: number, offsetY: number): number;
protected abstract _sliderMousePosition(e: ISimplifiedMouseEvent): number;
protected abstract _sliderOrthogonalMousePosition(e: ISimplifiedMouseEvent): number;
protected abstract _updateScrollbarSize(size: number): void;
public abstract writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void;
}

View File

@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { StandardWheelEvent } from 'vs/base/browser/mouseEvent';
import { AbstractScrollbar, ISimplifiedMouseEvent, ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
import { ARROW_IMG_SIZE } from 'vs/base/browser/ui/scrollbar/scrollbarArrow';
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
import { INewScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
import { Codicon, registerIcon } from 'vs/base/common/codicons';
const scrollbarButtonLeftIcon = registerIcon('scrollbar-button-left', Codicon.triangleLeft);
const scrollbarButtonRightIcon = registerIcon('scrollbar-button-right', Codicon.triangleRight);
export class HorizontalScrollbar extends AbstractScrollbar {
constructor(scrollable: Scrollable, options: ScrollableElementResolvedOptions, host: ScrollbarHost) {
const scrollDimensions = scrollable.getScrollDimensions();
const scrollPosition = scrollable.getCurrentScrollPosition();
super({
lazyRender: options.lazyRender,
host: host,
scrollbarState: new ScrollbarState(
(options.horizontalHasArrows ? options.arrowSize : 0),
(options.horizontal === ScrollbarVisibility.Hidden ? 0 : options.horizontalScrollbarSize),
(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize),
scrollDimensions.width,
scrollDimensions.scrollWidth,
scrollPosition.scrollLeft
),
visibility: options.horizontal,
extraScrollbarClassName: 'horizontal',
scrollable: scrollable
});
if (options.horizontalHasArrows) {
let arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
let scrollbarDelta = (options.horizontalScrollbarSize - ARROW_IMG_SIZE) / 2;
this._createArrow({
className: 'scra',
icon: scrollbarButtonLeftIcon,
top: scrollbarDelta,
left: arrowDelta,
bottom: undefined,
right: undefined,
bgWidth: options.arrowSize,
bgHeight: options.horizontalScrollbarSize,
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 1, 0)),
});
this._createArrow({
className: 'scra',
icon: scrollbarButtonRightIcon,
top: scrollbarDelta,
left: undefined,
bottom: undefined,
right: arrowDelta,
bgWidth: options.arrowSize,
bgHeight: options.horizontalScrollbarSize,
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, -1, 0)),
});
}
this._createSlider(Math.floor((options.horizontalScrollbarSize - options.horizontalSliderSize) / 2), 0, undefined, options.horizontalSliderSize);
}
protected _updateSlider(sliderSize: number, sliderPosition: number): void {
this.slider.setWidth(sliderSize);
this.slider.setLeft(sliderPosition);
}
protected _renderDomNode(largeSize: number, smallSize: number): void {
this.domNode.setWidth(largeSize);
this.domNode.setHeight(smallSize);
this.domNode.setLeft(0);
this.domNode.setBottom(0);
}
public onDidScroll(e: ScrollEvent): boolean {
this._shouldRender = this._onElementScrollSize(e.scrollWidth) || this._shouldRender;
this._shouldRender = this._onElementScrollPosition(e.scrollLeft) || this._shouldRender;
this._shouldRender = this._onElementSize(e.width) || this._shouldRender;
return this._shouldRender;
}
protected _mouseDownRelativePosition(offsetX: number, offsetY: number): number {
return offsetX;
}
protected _sliderMousePosition(e: ISimplifiedMouseEvent): number {
return e.posx;
}
protected _sliderOrthogonalMousePosition(e: ISimplifiedMouseEvent): number {
return e.posy;
}
protected _updateScrollbarSize(size: number): void {
this.slider.setHeight(size);
}
public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void {
target.scrollLeft = scrollPosition;
}
}

View File

@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Arrows */
.monaco-scrollable-element > .scrollbar > .scra {
cursor: pointer;
font-size: 11px !important;
}
.monaco-scrollable-element > .visible {
opacity: 1;
/* Background rule added for IE9 - to allow clicks on dom node */
background:rgba(0,0,0,0);
transition: opacity 100ms linear;
}
.monaco-scrollable-element > .invisible {
opacity: 0;
pointer-events: none;
}
.monaco-scrollable-element > .invisible.fade {
transition: opacity 800ms linear;
}
/* Scrollable Content Inset Shadow */
.monaco-scrollable-element > .shadow {
position: absolute;
display: none;
}
.monaco-scrollable-element > .shadow.top {
display: block;
top: 0;
left: 3px;
height: 3px;
width: 100%;
box-shadow: #DDD 0 6px 6px -6px inset;
}
.monaco-scrollable-element > .shadow.left {
display: block;
top: 3px;
left: 0;
height: 100%;
width: 3px;
box-shadow: #DDD 6px 0 6px -6px inset;
}
.monaco-scrollable-element > .shadow.top-left-corner {
display: block;
top: 0;
left: 0;
height: 3px;
width: 3px;
}
.monaco-scrollable-element > .shadow.top.left {
box-shadow: #DDD 6px 6px 6px -6px inset;
}
/* ---------- Default Style ---------- */
.vs .monaco-scrollable-element > .scrollbar > .slider {
background: rgba(100, 100, 100, .4);
}
.vs-dark .monaco-scrollable-element > .scrollbar > .slider {
background: rgba(121, 121, 121, .4);
}
.hc-black .monaco-scrollable-element > .scrollbar > .slider {
background: rgba(111, 195, 223, .6);
}
.monaco-scrollable-element > .scrollbar > .slider:hover {
background: rgba(100, 100, 100, .7);
}
.hc-black .monaco-scrollable-element > .scrollbar > .slider:hover {
background: rgba(111, 195, 223, .8);
}
.monaco-scrollable-element > .scrollbar > .slider.active {
background: rgba(0, 0, 0, .6);
}
.vs-dark .monaco-scrollable-element > .scrollbar > .slider.active {
background: rgba(191, 191, 191, .4);
}
.hc-black .monaco-scrollable-element > .scrollbar > .slider.active {
background: rgba(111, 195, 223, 1);
}
.vs-dark .monaco-scrollable-element .shadow.top {
box-shadow: none;
}
.vs-dark .monaco-scrollable-element .shadow.left {
box-shadow: #000 6px 0 6px -6px inset;
}
.vs-dark .monaco-scrollable-element .shadow.top.left {
box-shadow: #000 6px 6px 6px -6px inset;
}
.hc-black .monaco-scrollable-element .shadow.top {
box-shadow: none;
}
.hc-black .monaco-scrollable-element .shadow.left {
box-shadow: none;
}
.hc-black .monaco-scrollable-element .shadow.top.left {
box-shadow: none;
}

View File

@ -0,0 +1,629 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/scrollbars';
import * as dom from 'vs/base/browser/dom';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IMouseEvent, StandardWheelEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
import { HorizontalScrollbar } from 'vs/base/browser/ui/scrollbar/horizontalScrollbar';
import { ScrollableElementChangeOptions, ScrollableElementCreationOptions, ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
import { VerticalScrollbar } from 'vs/base/browser/ui/scrollbar/verticalScrollbar';
import { Widget } from 'vs/base/browser/ui/widget';
import { TimeoutTimer } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import * as platform from 'vs/base/common/platform';
import { INewScrollDimensions, INewScrollPosition, IScrollDimensions, IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
import { getZoomFactor } from 'vs/base/browser/browser';
const HIDE_TIMEOUT = 500;
const SCROLL_WHEEL_SENSITIVITY = 50;
const SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED = true;
export interface IOverviewRulerLayoutInfo {
parent: HTMLElement;
insertBefore: HTMLElement;
}
class MouseWheelClassifierItem {
public timestamp: number;
public deltaX: number;
public deltaY: number;
public score: number;
constructor(timestamp: number, deltaX: number, deltaY: number) {
this.timestamp = timestamp;
this.deltaX = deltaX;
this.deltaY = deltaY;
this.score = 0;
}
}
export class MouseWheelClassifier {
public static readonly INSTANCE = new MouseWheelClassifier();
private readonly _capacity: number;
private _memory: MouseWheelClassifierItem[];
private _front: number;
private _rear: number;
constructor() {
this._capacity = 5;
this._memory = [];
this._front = -1;
this._rear = -1;
}
public isPhysicalMouseWheel(): boolean {
if (this._front === -1 && this._rear === -1) {
// no elements
return false;
}
// 0.5 * last + 0.25 * 2nd last + 0.125 * 3rd last + ...
let remainingInfluence = 1;
let score = 0;
let iteration = 1;
let index = this._rear;
do {
const influence = (index === this._front ? remainingInfluence : Math.pow(2, -iteration));
remainingInfluence -= influence;
score += this._memory[index].score * influence;
if (index === this._front) {
break;
}
index = (this._capacity + index - 1) % this._capacity;
iteration++;
} while (true);
return (score <= 0.5);
}
public accept(timestamp: number, deltaX: number, deltaY: number): void {
const item = new MouseWheelClassifierItem(timestamp, deltaX, deltaY);
item.score = this._computeScore(item);
if (this._front === -1 && this._rear === -1) {
this._memory[0] = item;
this._front = 0;
this._rear = 0;
} else {
this._rear = (this._rear + 1) % this._capacity;
if (this._rear === this._front) {
// Drop oldest
this._front = (this._front + 1) % this._capacity;
}
this._memory[this._rear] = item;
}
}
/**
* A score between 0 and 1 for `item`.
* - a score towards 0 indicates that the source appears to be a physical mouse wheel
* - a score towards 1 indicates that the source appears to be a touchpad or magic mouse, etc.
*/
private _computeScore(item: MouseWheelClassifierItem): number {
if (Math.abs(item.deltaX) > 0 && Math.abs(item.deltaY) > 0) {
// both axes exercised => definitely not a physical mouse wheel
return 1;
}
let score: number = 0.5;
const prev = (this._front === -1 && this._rear === -1 ? null : this._memory[this._rear]);
if (prev) {
// const deltaT = item.timestamp - prev.timestamp;
// if (deltaT < 1000 / 30) {
// // sooner than X times per second => indicator that this is not a physical mouse wheel
// score += 0.25;
// }
// if (item.deltaX === prev.deltaX && item.deltaY === prev.deltaY) {
// // equal amplitude => indicator that this is a physical mouse wheel
// score -= 0.25;
// }
}
if (!this._isAlmostInt(item.deltaX) || !this._isAlmostInt(item.deltaY)) {
// non-integer deltas => indicator that this is not a physical mouse wheel
score += 0.25;
}
return Math.min(Math.max(score, 0), 1);
}
private _isAlmostInt(value: number): boolean {
const delta = Math.abs(Math.round(value) - value);
return (delta < 0.01);
}
}
export abstract class AbstractScrollableElement extends Widget {
private readonly _options: ScrollableElementResolvedOptions;
protected readonly _scrollable: Scrollable;
private readonly _verticalScrollbar: VerticalScrollbar;
private readonly _horizontalScrollbar: HorizontalScrollbar;
private readonly _domNode: HTMLElement;
private readonly _leftShadowDomNode: FastDomNode<HTMLElement> | null;
private readonly _topShadowDomNode: FastDomNode<HTMLElement> | null;
private readonly _topLeftShadowDomNode: FastDomNode<HTMLElement> | null;
private readonly _listenOnDomNode: HTMLElement;
private _mouseWheelToDispose: IDisposable[];
private _isDragging: boolean;
private _mouseIsOver: boolean;
private readonly _hideTimeout: TimeoutTimer;
private _shouldRender: boolean;
private _revealOnScroll: boolean;
private readonly _onScroll = this._register(new Emitter<ScrollEvent>());
public readonly onScroll: Event<ScrollEvent> = this._onScroll.event;
private readonly _onWillScroll = this._register(new Emitter<ScrollEvent>());
public readonly onWillScroll: Event<ScrollEvent> = this._onWillScroll.event;
protected constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable: Scrollable) {
super();
element.style.overflow = 'hidden';
this._options = resolveOptions(options);
this._scrollable = scrollable;
this._register(this._scrollable.onScroll((e) => {
this._onWillScroll.fire(e);
this._onDidScroll(e);
this._onScroll.fire(e);
}));
let scrollbarHost: ScrollbarHost = {
onMouseWheel: (mouseWheelEvent: StandardWheelEvent) => this._onMouseWheel(mouseWheelEvent),
onDragStart: () => this._onDragStart(),
onDragEnd: () => this._onDragEnd(),
};
this._verticalScrollbar = this._register(new VerticalScrollbar(this._scrollable, this._options, scrollbarHost));
this._horizontalScrollbar = this._register(new HorizontalScrollbar(this._scrollable, this._options, scrollbarHost));
this._domNode = document.createElement('div');
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
this._domNode.setAttribute('role', 'presentation');
this._domNode.style.position = 'relative';
this._domNode.style.overflow = 'hidden';
this._domNode.appendChild(element);
this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode);
this._domNode.appendChild(this._verticalScrollbar.domNode.domNode);
if (this._options.useShadows) {
this._leftShadowDomNode = createFastDomNode(document.createElement('div'));
this._leftShadowDomNode.setClassName('shadow');
this._domNode.appendChild(this._leftShadowDomNode.domNode);
this._topShadowDomNode = createFastDomNode(document.createElement('div'));
this._topShadowDomNode.setClassName('shadow');
this._domNode.appendChild(this._topShadowDomNode.domNode);
this._topLeftShadowDomNode = createFastDomNode(document.createElement('div'));
this._topLeftShadowDomNode.setClassName('shadow top-left-corner');
this._domNode.appendChild(this._topLeftShadowDomNode.domNode);
} else {
this._leftShadowDomNode = null;
this._topShadowDomNode = null;
this._topLeftShadowDomNode = null;
}
this._listenOnDomNode = this._options.listenOnDomNode || this._domNode;
this._mouseWheelToDispose = [];
this._setListeningToMouseWheel(this._options.handleMouseWheel);
this.onmouseover(this._listenOnDomNode, (e) => this._onMouseOver(e));
this.onnonbubblingmouseout(this._listenOnDomNode, (e) => this._onMouseOut(e));
this._hideTimeout = this._register(new TimeoutTimer());
this._isDragging = false;
this._mouseIsOver = false;
this._shouldRender = true;
this._revealOnScroll = true;
}
public dispose(): void {
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
super.dispose();
}
/**
* Get the generated 'scrollable' dom node
*/
public getDomNode(): HTMLElement {
return this._domNode;
}
public getOverviewRulerLayoutInfo(): IOverviewRulerLayoutInfo {
return {
parent: this._domNode,
insertBefore: this._verticalScrollbar.domNode.domNode,
};
}
/**
* Delegate a mouse down event to the vertical scrollbar.
* This is to help with clicking somewhere else and having the scrollbar react.
*/
public delegateVerticalScrollbarMouseDown(browserEvent: IMouseEvent): void {
this._verticalScrollbar.delegateMouseDown(browserEvent);
}
public getScrollDimensions(): IScrollDimensions {
return this._scrollable.getScrollDimensions();
}
public setScrollDimensions(dimensions: INewScrollDimensions): void {
this._scrollable.setScrollDimensions(dimensions, false);
}
/**
* Update the class name of the scrollable element.
*/
public updateClassName(newClassName: string): void {
this._options.className = newClassName;
// Defaults are different on Macs
if (platform.isMacintosh) {
this._options.className += ' mac';
}
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
}
/**
* Update configuration options for the scrollbar.
* Really this is Editor.IEditorScrollbarOptions, but base shouldn't
* depend on Editor.
*/
public updateOptions(newOptions: ScrollableElementChangeOptions): void {
if (typeof newOptions.handleMouseWheel !== 'undefined') {
this._options.handleMouseWheel = newOptions.handleMouseWheel;
this._setListeningToMouseWheel(this._options.handleMouseWheel);
}
if (typeof newOptions.mouseWheelScrollSensitivity !== 'undefined') {
this._options.mouseWheelScrollSensitivity = newOptions.mouseWheelScrollSensitivity;
}
if (typeof newOptions.fastScrollSensitivity !== 'undefined') {
this._options.fastScrollSensitivity = newOptions.fastScrollSensitivity;
}
if (typeof newOptions.scrollPredominantAxis !== 'undefined') {
this._options.scrollPredominantAxis = newOptions.scrollPredominantAxis;
}
if (typeof newOptions.horizontalScrollbarSize !== 'undefined') {
this._horizontalScrollbar.updateScrollbarSize(newOptions.horizontalScrollbarSize);
}
if (!this._options.lazyRender) {
this._render();
}
}
public setRevealOnScroll(value: boolean) {
this._revealOnScroll = value;
}
public triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {
this._onMouseWheel(new StandardWheelEvent(browserEvent));
}
// -------------------- mouse wheel scrolling --------------------
private _setListeningToMouseWheel(shouldListen: boolean): void {
let isListening = (this._mouseWheelToDispose.length > 0);
if (isListening === shouldListen) {
// No change
return;
}
// Stop listening (if necessary)
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
// Start listening (if necessary)
if (shouldListen) {
let onMouseWheel = (browserEvent: IMouseWheelEvent) => {
this._onMouseWheel(new StandardWheelEvent(browserEvent));
};
this._mouseWheelToDispose.push(dom.addDisposableListener(this._listenOnDomNode, dom.EventType.MOUSE_WHEEL, onMouseWheel, { passive: false }));
}
}
private _onMouseWheel(e: StandardWheelEvent): void {
const classifier = MouseWheelClassifier.INSTANCE;
if (SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED) {
const osZoomFactor = window.devicePixelRatio / getZoomFactor();
if (platform.isWindows || platform.isLinux) {
// On Windows and Linux, the incoming delta events are multiplied with the OS zoom factor.
// The OS zoom factor can be reverse engineered by using the device pixel ratio and the configured zoom factor into account.
classifier.accept(Date.now(), e.deltaX / osZoomFactor, e.deltaY / osZoomFactor);
} else {
classifier.accept(Date.now(), e.deltaX, e.deltaY);
}
}
// console.log(`${Date.now()}, ${e.deltaY}, ${e.deltaX}`);
if (e.deltaY || e.deltaX) {
let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity;
let deltaX = e.deltaX * this._options.mouseWheelScrollSensitivity;
if (this._options.scrollPredominantAxis) {
if (Math.abs(deltaY) >= Math.abs(deltaX)) {
deltaX = 0;
} else {
deltaY = 0;
}
}
if (this._options.flipAxes) {
[deltaY, deltaX] = [deltaX, deltaY];
}
// Convert vertical scrolling to horizontal if shift is held, this
// is handled at a higher level on Mac
const shiftConvert = !platform.isMacintosh && e.browserEvent && e.browserEvent.shiftKey;
if ((this._options.scrollYToX || shiftConvert) && !deltaX) {
deltaX = deltaY;
deltaY = 0;
}
if (e.browserEvent && e.browserEvent.altKey) {
// fastScrolling
deltaX = deltaX * this._options.fastScrollSensitivity;
deltaY = deltaY * this._options.fastScrollSensitivity;
}
const futureScrollPosition = this._scrollable.getFutureScrollPosition();
let desiredScrollPosition: INewScrollPosition = {};
if (deltaY) {
const desiredScrollTop = futureScrollPosition.scrollTop - SCROLL_WHEEL_SENSITIVITY * deltaY;
this._verticalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollTop);
}
if (deltaX) {
const desiredScrollLeft = futureScrollPosition.scrollLeft - SCROLL_WHEEL_SENSITIVITY * deltaX;
this._horizontalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollLeft);
}
// Check that we are scrolling towards a location which is valid
desiredScrollPosition = this._scrollable.validateScrollPosition(desiredScrollPosition);
if (futureScrollPosition.scrollLeft !== desiredScrollPosition.scrollLeft || futureScrollPosition.scrollTop !== desiredScrollPosition.scrollTop) {
const canPerformSmoothScroll = (
SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED
&& this._options.mouseWheelSmoothScroll
&& classifier.isPhysicalMouseWheel()
);
if (canPerformSmoothScroll) {
this._scrollable.setScrollPositionSmooth(desiredScrollPosition);
} else {
this._scrollable.setScrollPositionNow(desiredScrollPosition);
}
this._shouldRender = true;
}
}
if (this._options.alwaysConsumeMouseWheel || this._shouldRender) {
e.preventDefault();
e.stopPropagation();
}
}
private _onDidScroll(e: ScrollEvent): void {
this._shouldRender = this._horizontalScrollbar.onDidScroll(e) || this._shouldRender;
this._shouldRender = this._verticalScrollbar.onDidScroll(e) || this._shouldRender;
if (this._options.useShadows) {
this._shouldRender = true;
}
if (this._revealOnScroll) {
this._reveal();
}
if (!this._options.lazyRender) {
this._render();
}
}
/**
* Render / mutate the DOM now.
* Should be used together with the ctor option `lazyRender`.
*/
public renderNow(): void {
if (!this._options.lazyRender) {
throw new Error('Please use `lazyRender` together with `renderNow`!');
}
this._render();
}
private _render(): void {
if (!this._shouldRender) {
return;
}
this._shouldRender = false;
this._horizontalScrollbar.render();
this._verticalScrollbar.render();
if (this._options.useShadows) {
const scrollState = this._scrollable.getCurrentScrollPosition();
let enableTop = scrollState.scrollTop > 0;
let enableLeft = scrollState.scrollLeft > 0;
this._leftShadowDomNode!.setClassName('shadow' + (enableLeft ? ' left' : ''));
this._topShadowDomNode!.setClassName('shadow' + (enableTop ? ' top' : ''));
this._topLeftShadowDomNode!.setClassName('shadow top-left-corner' + (enableTop ? ' top' : '') + (enableLeft ? ' left' : ''));
}
}
// -------------------- fade in / fade out --------------------
private _onDragStart(): void {
this._isDragging = true;
this._reveal();
}
private _onDragEnd(): void {
this._isDragging = false;
this._hide();
}
private _onMouseOut(e: IMouseEvent): void {
this._mouseIsOver = false;
this._hide();
}
private _onMouseOver(e: IMouseEvent): void {
this._mouseIsOver = true;
this._reveal();
}
private _reveal(): void {
this._verticalScrollbar.beginReveal();
this._horizontalScrollbar.beginReveal();
this._scheduleHide();
}
private _hide(): void {
if (!this._mouseIsOver && !this._isDragging) {
this._verticalScrollbar.beginHide();
this._horizontalScrollbar.beginHide();
}
}
private _scheduleHide(): void {
if (!this._mouseIsOver && !this._isDragging) {
this._hideTimeout.cancelAndSet(() => this._hide(), HIDE_TIMEOUT);
}
}
}
export class ScrollableElement extends AbstractScrollableElement {
constructor(element: HTMLElement, options: ScrollableElementCreationOptions) {
options = options || {};
options.mouseWheelSmoothScroll = false;
const scrollable = new Scrollable(0, (callback) => dom.scheduleAtNextAnimationFrame(callback));
super(element, options, scrollable);
this._register(scrollable);
}
public setScrollPosition(update: INewScrollPosition): void {
this._scrollable.setScrollPositionNow(update);
}
public getScrollPosition(): IScrollPosition {
return this._scrollable.getCurrentScrollPosition();
}
}
export class SmoothScrollableElement extends AbstractScrollableElement {
constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable: Scrollable) {
super(element, options, scrollable);
}
public setScrollPosition(update: INewScrollPosition): void {
this._scrollable.setScrollPositionNow(update);
}
public getScrollPosition(): IScrollPosition {
return this._scrollable.getCurrentScrollPosition();
}
}
export class DomScrollableElement extends ScrollableElement {
private _element: HTMLElement;
constructor(element: HTMLElement, options: ScrollableElementCreationOptions) {
super(element, options);
this._element = element;
this.onScroll((e) => {
if (e.scrollTopChanged) {
this._element.scrollTop = e.scrollTop;
}
if (e.scrollLeftChanged) {
this._element.scrollLeft = e.scrollLeft;
}
});
this.scanDomNode();
}
public scanDomNode(): void {
// width, scrollLeft, scrollWidth, height, scrollTop, scrollHeight
this.setScrollDimensions({
width: this._element.clientWidth,
scrollWidth: this._element.scrollWidth,
height: this._element.clientHeight,
scrollHeight: this._element.scrollHeight
});
this.setScrollPosition({
scrollLeft: this._element.scrollLeft,
scrollTop: this._element.scrollTop,
});
}
}
function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableElementResolvedOptions {
let result: ScrollableElementResolvedOptions = {
lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false),
className: (typeof opts.className !== 'undefined' ? opts.className : ''),
useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true),
handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true),
flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false),
alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false),
scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),
fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5),
scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true),
mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true),
arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11),
listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null),
horizontal: (typeof opts.horizontal !== 'undefined' ? opts.horizontal : ScrollbarVisibility.Auto),
horizontalScrollbarSize: (typeof opts.horizontalScrollbarSize !== 'undefined' ? opts.horizontalScrollbarSize : 10),
horizontalSliderSize: (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : 0),
horizontalHasArrows: (typeof opts.horizontalHasArrows !== 'undefined' ? opts.horizontalHasArrows : false),
vertical: (typeof opts.vertical !== 'undefined' ? opts.vertical : ScrollbarVisibility.Auto),
verticalScrollbarSize: (typeof opts.verticalScrollbarSize !== 'undefined' ? opts.verticalScrollbarSize : 10),
verticalHasArrows: (typeof opts.verticalHasArrows !== 'undefined' ? opts.verticalHasArrows : false),
verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0)
};
result.horizontalSliderSize = (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : result.horizontalScrollbarSize);
result.verticalSliderSize = (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : result.verticalScrollbarSize);
// Defaults are different on Macs
if (platform.isMacintosh) {
result.className += ' mac';
}
return result;
}

View File

@ -0,0 +1,149 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
export interface ScrollableElementCreationOptions {
/**
* The scrollable element should not do any DOM mutations until renderNow() is called.
* Defaults to false.
*/
lazyRender?: boolean;
/**
* CSS Class name for the scrollable element.
*/
className?: string;
/**
* Drop subtle horizontal and vertical shadows.
* Defaults to false.
*/
useShadows?: boolean;
/**
* Handle mouse wheel (listen to mouse wheel scrolling).
* Defaults to true
*/
handleMouseWheel?: boolean;
/**
* If mouse wheel is handled, make mouse wheel scrolling smooth.
* Defaults to true.
*/
mouseWheelSmoothScroll?: boolean;
/**
* Flip axes. Treat vertical scrolling like horizontal and vice-versa.
* Defaults to false.
*/
flipAxes?: boolean;
/**
* If enabled, will scroll horizontally when scrolling vertical.
* Defaults to false.
*/
scrollYToX?: boolean;
/**
* Always consume mouse wheel events, even when scrolling is no longer possible.
* Defaults to false.
*/
alwaysConsumeMouseWheel?: boolean;
/**
* A multiplier to be used on the `deltaX` and `deltaY` of mouse wheel scroll events.
* Defaults to 1.
*/
mouseWheelScrollSensitivity?: number;
/**
* FastScrolling mulitplier speed when pressing `Alt`
* Defaults to 5.
*/
fastScrollSensitivity?: number;
/**
* Whether the scrollable will only scroll along the predominant axis when scrolling both
* vertically and horizontally at the same time.
* Prevents horizontal drift when scrolling vertically on a trackpad.
* Defaults to true.
*/
scrollPredominantAxis?: boolean;
/**
* Height for vertical arrows (top/bottom) and width for horizontal arrows (left/right).
* Defaults to 11.
*/
arrowSize?: number;
/**
* The dom node events should be bound to.
* If no listenOnDomNode is provided, the dom node passed to the constructor will be used for event listening.
*/
listenOnDomNode?: HTMLElement;
/**
* Control the visibility of the horizontal scrollbar.
* Accepted values: 'auto' (on mouse over), 'visible' (always visible), 'hidden' (never visible)
* Defaults to 'auto'.
*/
horizontal?: ScrollbarVisibility;
/**
* Height (in px) of the horizontal scrollbar.
* Defaults to 10.
*/
horizontalScrollbarSize?: number;
/**
* Height (in px) of the horizontal scrollbar slider.
* Defaults to `horizontalScrollbarSize`
*/
horizontalSliderSize?: number;
/**
* Render arrows (left/right) for the horizontal scrollbar.
* Defaults to false.
*/
horizontalHasArrows?: boolean;
/**
* Control the visibility of the vertical scrollbar.
* Accepted values: 'auto' (on mouse over), 'visible' (always visible), 'hidden' (never visible)
* Defaults to 'auto'.
*/
vertical?: ScrollbarVisibility;
/**
* Width (in px) of the vertical scrollbar.
* Defaults to 10.
*/
verticalScrollbarSize?: number;
/**
* Width (in px) of the vertical scrollbar slider.
* Defaults to `verticalScrollbarSize`
*/
verticalSliderSize?: number;
/**
* Render arrows (top/bottom) for the vertical scrollbar.
* Defaults to false.
*/
verticalHasArrows?: boolean;
}
export interface ScrollableElementChangeOptions {
handleMouseWheel?: boolean;
mouseWheelScrollSensitivity?: number;
fastScrollSensitivity?: number;
scrollPredominantAxis?: boolean;
horizontalScrollbarSize?: number;
}
export interface ScrollableElementResolvedOptions {
lazyRender: boolean;
className: string;
useShadows: boolean;
handleMouseWheel: boolean;
flipAxes: boolean;
scrollYToX: boolean;
alwaysConsumeMouseWheel: boolean;
mouseWheelScrollSensitivity: number;
fastScrollSensitivity: number;
scrollPredominantAxis: boolean;
mouseWheelSmoothScroll: boolean;
arrowSize: number;
listenOnDomNode: HTMLElement | null;
horizontal: ScrollbarVisibility;
horizontalScrollbarSize: number;
horizontalSliderSize: number;
horizontalHasArrows: boolean;
vertical: ScrollbarVisibility;
verticalScrollbarSize: number;
verticalSliderSize: number;
verticalHasArrows: boolean;
}

View File

@ -0,0 +1,114 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { Widget } from 'vs/base/browser/ui/widget';
import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async';
import { Codicon } from 'vs/base/common/codicons';
/**
* The arrow image size.
*/
export const ARROW_IMG_SIZE = 11;
export interface ScrollbarArrowOptions {
onActivate: () => void;
className: string;
icon: Codicon;
bgWidth: number;
bgHeight: number;
top?: number;
left?: number;
bottom?: number;
right?: number;
}
export class ScrollbarArrow extends Widget {
private _onActivate: () => void;
public bgDomNode: HTMLElement;
public domNode: HTMLElement;
private _mousedownRepeatTimer: IntervalTimer;
private _mousedownScheduleRepeatTimer: TimeoutTimer;
private _mouseMoveMonitor: GlobalMouseMoveMonitor<IStandardMouseMoveEventData>;
constructor(opts: ScrollbarArrowOptions) {
super();
this._onActivate = opts.onActivate;
this.bgDomNode = document.createElement('div');
this.bgDomNode.className = 'arrow-background';
this.bgDomNode.style.position = 'absolute';
this.bgDomNode.style.width = opts.bgWidth + 'px';
this.bgDomNode.style.height = opts.bgHeight + 'px';
if (typeof opts.top !== 'undefined') {
this.bgDomNode.style.top = '0px';
}
if (typeof opts.left !== 'undefined') {
this.bgDomNode.style.left = '0px';
}
if (typeof opts.bottom !== 'undefined') {
this.bgDomNode.style.bottom = '0px';
}
if (typeof opts.right !== 'undefined') {
this.bgDomNode.style.right = '0px';
}
this.domNode = document.createElement('div');
this.domNode.className = opts.className;
this.domNode.classList.add(...opts.icon.classNamesArray);
this.domNode.style.position = 'absolute';
this.domNode.style.width = ARROW_IMG_SIZE + 'px';
this.domNode.style.height = ARROW_IMG_SIZE + 'px';
if (typeof opts.top !== 'undefined') {
this.domNode.style.top = opts.top + 'px';
}
if (typeof opts.left !== 'undefined') {
this.domNode.style.left = opts.left + 'px';
}
if (typeof opts.bottom !== 'undefined') {
this.domNode.style.bottom = opts.bottom + 'px';
}
if (typeof opts.right !== 'undefined') {
this.domNode.style.right = opts.right + 'px';
}
this._mouseMoveMonitor = this._register(new GlobalMouseMoveMonitor<IStandardMouseMoveEventData>());
this.onmousedown(this.bgDomNode, (e) => this._arrowMouseDown(e));
this.onmousedown(this.domNode, (e) => this._arrowMouseDown(e));
this._mousedownRepeatTimer = this._register(new IntervalTimer());
this._mousedownScheduleRepeatTimer = this._register(new TimeoutTimer());
}
private _arrowMouseDown(e: IMouseEvent): void {
let scheduleRepeater = () => {
this._mousedownRepeatTimer.cancelAndSet(() => this._onActivate(), 1000 / 24);
};
this._onActivate();
this._mousedownRepeatTimer.cancel();
this._mousedownScheduleRepeatTimer.cancelAndSet(scheduleRepeater, 200);
this._mouseMoveMonitor.startMonitoring(
e.target,
e.buttons,
standardMouseMoveMerger,
(mouseMoveData: IStandardMouseMoveEventData) => {
/* Intentional empty */
},
() => {
this._mousedownRepeatTimer.cancel();
this._mousedownScheduleRepeatTimer.cancel();
}
);
e.preventDefault();
}
}

View File

@ -0,0 +1,217 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* The minimal size of the slider (such that it can still be clickable) -- it is artificially enlarged.
*/
const MINIMUM_SLIDER_SIZE = 20;
export class ScrollbarState {
/**
* For the vertical scrollbar: the width.
* For the horizontal scrollbar: the height.
*/
private _scrollbarSize: number;
/**
* For the vertical scrollbar: the height of the pair horizontal scrollbar.
* For the horizontal scrollbar: the width of the pair vertical scrollbar.
*/
private readonly _oppositeScrollbarSize: number;
/**
* For the vertical scrollbar: the height of the scrollbar's arrows.
* For the horizontal scrollbar: the width of the scrollbar's arrows.
*/
private readonly _arrowSize: number;
// --- variables
/**
* For the vertical scrollbar: the viewport height.
* For the horizontal scrollbar: the viewport width.
*/
private _visibleSize: number;
/**
* For the vertical scrollbar: the scroll height.
* For the horizontal scrollbar: the scroll width.
*/
private _scrollSize: number;
/**
* For the vertical scrollbar: the scroll top.
* For the horizontal scrollbar: the scroll left.
*/
private _scrollPosition: number;
// --- computed variables
/**
* `visibleSize` - `oppositeScrollbarSize`
*/
private _computedAvailableSize: number;
/**
* (`scrollSize` > 0 && `scrollSize` > `visibleSize`)
*/
private _computedIsNeeded: boolean;
private _computedSliderSize: number;
private _computedSliderRatio: number;
private _computedSliderPosition: number;
constructor(arrowSize: number, scrollbarSize: number, oppositeScrollbarSize: number, visibleSize: number, scrollSize: number, scrollPosition: number) {
this._scrollbarSize = Math.round(scrollbarSize);
this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
this._arrowSize = Math.round(arrowSize);
this._visibleSize = visibleSize;
this._scrollSize = scrollSize;
this._scrollPosition = scrollPosition;
this._computedAvailableSize = 0;
this._computedIsNeeded = false;
this._computedSliderSize = 0;
this._computedSliderRatio = 0;
this._computedSliderPosition = 0;
this._refreshComputedValues();
}
public clone(): ScrollbarState {
return new ScrollbarState(this._arrowSize, this._scrollbarSize, this._oppositeScrollbarSize, this._visibleSize, this._scrollSize, this._scrollPosition);
}
public setVisibleSize(visibleSize: number): boolean {
let iVisibleSize = Math.round(visibleSize);
if (this._visibleSize !== iVisibleSize) {
this._visibleSize = iVisibleSize;
this._refreshComputedValues();
return true;
}
return false;
}
public setScrollSize(scrollSize: number): boolean {
let iScrollSize = Math.round(scrollSize);
if (this._scrollSize !== iScrollSize) {
this._scrollSize = iScrollSize;
this._refreshComputedValues();
return true;
}
return false;
}
public setScrollPosition(scrollPosition: number): boolean {
let iScrollPosition = Math.round(scrollPosition);
if (this._scrollPosition !== iScrollPosition) {
this._scrollPosition = iScrollPosition;
this._refreshComputedValues();
return true;
}
return false;
}
public setScrollbarSize(scrollbarSize: number): void {
this._scrollbarSize = scrollbarSize;
}
private static _computeValues(oppositeScrollbarSize: number, arrowSize: number, visibleSize: number, scrollSize: number, scrollPosition: number) {
const computedAvailableSize = Math.max(0, visibleSize - oppositeScrollbarSize);
const computedRepresentableSize = Math.max(0, computedAvailableSize - 2 * arrowSize);
const computedIsNeeded = (scrollSize > 0 && scrollSize > visibleSize);
if (!computedIsNeeded) {
// There is no need for a slider
return {
computedAvailableSize: Math.round(computedAvailableSize),
computedIsNeeded: computedIsNeeded,
computedSliderSize: Math.round(computedRepresentableSize),
computedSliderRatio: 0,
computedSliderPosition: 0,
};
}
// We must artificially increase the size of the slider if needed, since the slider would be too small to grab with the mouse otherwise
const computedSliderSize = Math.round(Math.max(MINIMUM_SLIDER_SIZE, Math.floor(visibleSize * computedRepresentableSize / scrollSize)));
// The slider can move from 0 to `computedRepresentableSize` - `computedSliderSize`
// in the same way `scrollPosition` can move from 0 to `scrollSize` - `visibleSize`.
const computedSliderRatio = (computedRepresentableSize - computedSliderSize) / (scrollSize - visibleSize);
const computedSliderPosition = (scrollPosition * computedSliderRatio);
return {
computedAvailableSize: Math.round(computedAvailableSize),
computedIsNeeded: computedIsNeeded,
computedSliderSize: Math.round(computedSliderSize),
computedSliderRatio: computedSliderRatio,
computedSliderPosition: Math.round(computedSliderPosition),
};
}
private _refreshComputedValues(): void {
const r = ScrollbarState._computeValues(this._oppositeScrollbarSize, this._arrowSize, this._visibleSize, this._scrollSize, this._scrollPosition);
this._computedAvailableSize = r.computedAvailableSize;
this._computedIsNeeded = r.computedIsNeeded;
this._computedSliderSize = r.computedSliderSize;
this._computedSliderRatio = r.computedSliderRatio;
this._computedSliderPosition = r.computedSliderPosition;
}
public getArrowSize(): number {
return this._arrowSize;
}
public getScrollPosition(): number {
return this._scrollPosition;
}
public getRectangleLargeSize(): number {
return this._computedAvailableSize;
}
public getRectangleSmallSize(): number {
return this._scrollbarSize;
}
public isNeeded(): boolean {
return this._computedIsNeeded;
}
public getSliderSize(): number {
return this._computedSliderSize;
}
public getSliderPosition(): number {
return this._computedSliderPosition;
}
/**
* Compute a desired `scrollPosition` such that `offset` ends up in the center of the slider.
* `offset` is based on the same coordinate system as the `sliderPosition`.
*/
public getDesiredScrollPositionFromOffset(offset: number): number {
if (!this._computedIsNeeded) {
// no need for a slider
return 0;
}
let desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
/**
* Compute a desired `scrollPosition` such that the slider moves by `delta`.
*/
public getDesiredScrollPositionFromDelta(delta: number): number {
if (!this._computedIsNeeded) {
// no need for a slider
return 0;
}
let desiredSliderPosition = this._computedSliderPosition + delta;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
}

View File

@ -0,0 +1,108 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { FastDomNode } from 'vs/base/browser/fastDomNode';
import { TimeoutTimer } from 'vs/base/common/async';
import { Disposable } from 'vs/base/common/lifecycle';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
export class ScrollbarVisibilityController extends Disposable {
private _visibility: ScrollbarVisibility;
private _visibleClassName: string;
private _invisibleClassName: string;
private _domNode: FastDomNode<HTMLElement> | null;
private _shouldBeVisible: boolean;
private _isNeeded: boolean;
private _isVisible: boolean;
private _revealTimer: TimeoutTimer;
constructor(visibility: ScrollbarVisibility, visibleClassName: string, invisibleClassName: string) {
super();
this._visibility = visibility;
this._visibleClassName = visibleClassName;
this._invisibleClassName = invisibleClassName;
this._domNode = null;
this._isVisible = false;
this._isNeeded = false;
this._shouldBeVisible = false;
this._revealTimer = this._register(new TimeoutTimer());
}
// ----------------- Hide / Reveal
private applyVisibilitySetting(shouldBeVisible: boolean): boolean {
if (this._visibility === ScrollbarVisibility.Hidden) {
return false;
}
if (this._visibility === ScrollbarVisibility.Visible) {
return true;
}
return shouldBeVisible;
}
public setShouldBeVisible(rawShouldBeVisible: boolean): void {
let shouldBeVisible = this.applyVisibilitySetting(rawShouldBeVisible);
if (this._shouldBeVisible !== shouldBeVisible) {
this._shouldBeVisible = shouldBeVisible;
this.ensureVisibility();
}
}
public setIsNeeded(isNeeded: boolean): void {
if (this._isNeeded !== isNeeded) {
this._isNeeded = isNeeded;
this.ensureVisibility();
}
}
public setDomNode(domNode: FastDomNode<HTMLElement>): void {
this._domNode = domNode;
this._domNode.setClassName(this._invisibleClassName);
// Now that the flags & the dom node are in a consistent state, ensure the Hidden/Visible configuration
this.setShouldBeVisible(false);
}
public ensureVisibility(): void {
if (!this._isNeeded) {
// Nothing to be rendered
this._hide(false);
return;
}
if (this._shouldBeVisible) {
this._reveal();
} else {
this._hide(true);
}
}
private _reveal(): void {
if (this._isVisible) {
return;
}
this._isVisible = true;
// The CSS animation doesn't play otherwise
this._revealTimer.setIfNotSet(() => {
if (this._domNode) {
this._domNode.setClassName(this._visibleClassName);
}
}, 0);
}
private _hide(withFadeAway: boolean): void {
this._revealTimer.cancel();
if (!this._isVisible) {
return;
}
this._isVisible = false;
if (this._domNode) {
this._domNode.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : ''));
}
}
}

View File

@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { StandardWheelEvent } from 'vs/base/browser/mouseEvent';
import { AbstractScrollbar, ISimplifiedMouseEvent, ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
import { ARROW_IMG_SIZE } from 'vs/base/browser/ui/scrollbar/scrollbarArrow';
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
import { INewScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
import { Codicon, registerIcon } from 'vs/base/common/codicons';
const scrollbarButtonUpIcon = registerIcon('scrollbar-button-up', Codicon.triangleUp);
const scrollbarButtonDownIcon = registerIcon('scrollbar-button-down', Codicon.triangleDown);
export class VerticalScrollbar extends AbstractScrollbar {
constructor(scrollable: Scrollable, options: ScrollableElementResolvedOptions, host: ScrollbarHost) {
const scrollDimensions = scrollable.getScrollDimensions();
const scrollPosition = scrollable.getCurrentScrollPosition();
super({
lazyRender: options.lazyRender,
host: host,
scrollbarState: new ScrollbarState(
(options.verticalHasArrows ? options.arrowSize : 0),
(options.vertical === ScrollbarVisibility.Hidden ? 0 : options.verticalScrollbarSize),
// give priority to vertical scroll bar over horizontal and let it scroll all the way to the bottom
0,
scrollDimensions.height,
scrollDimensions.scrollHeight,
scrollPosition.scrollTop
),
visibility: options.vertical,
extraScrollbarClassName: 'vertical',
scrollable: scrollable
});
if (options.verticalHasArrows) {
let arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
let scrollbarDelta = (options.verticalScrollbarSize - ARROW_IMG_SIZE) / 2;
this._createArrow({
className: 'scra',
icon: scrollbarButtonUpIcon,
top: arrowDelta,
left: scrollbarDelta,
bottom: undefined,
right: undefined,
bgWidth: options.verticalScrollbarSize,
bgHeight: options.arrowSize,
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 0, 1)),
});
this._createArrow({
className: 'scra',
icon: scrollbarButtonDownIcon,
top: undefined,
left: scrollbarDelta,
bottom: arrowDelta,
right: undefined,
bgWidth: options.verticalScrollbarSize,
bgHeight: options.arrowSize,
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 0, -1)),
});
}
this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, undefined);
}
protected _updateSlider(sliderSize: number, sliderPosition: number): void {
this.slider.setHeight(sliderSize);
this.slider.setTop(sliderPosition);
}
protected _renderDomNode(largeSize: number, smallSize: number): void {
this.domNode.setWidth(smallSize);
this.domNode.setHeight(largeSize);
this.domNode.setRight(0);
this.domNode.setTop(0);
}
public onDidScroll(e: ScrollEvent): boolean {
this._shouldRender = this._onElementScrollSize(e.scrollHeight) || this._shouldRender;
this._shouldRender = this._onElementScrollPosition(e.scrollTop) || this._shouldRender;
this._shouldRender = this._onElementSize(e.height) || this._shouldRender;
return this._shouldRender;
}
protected _mouseDownRelativePosition(offsetX: number, offsetY: number): number {
return offsetY;
}
protected _sliderMousePosition(e: ISimplifiedMouseEvent): number {
return e.posy;
}
protected _sliderOrthogonalMousePosition(e: ISimplifiedMouseEvent): number {
return e.posx;
}
protected _updateScrollbarSize(size: number): void {
this.slider.setWidth(size);
}
public writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void {
target.scrollTop = scrollPosition;
}
}

Some files were not shown because too many files have changed in this diff Show More