Files
compiler-explorer/static/utils.ts
Matt Godbolt (bot acct) 90e742a5e7 Add VS Code / JetBrains keymap setting (default VS Code) (#8682)
## Summary
- Adds a **Settings → Keybindings → Keymap** dropdown with `VS Code`
(default) and `JetBrains` options.
- Previously CE unconditionally bound `Ctrl+D` to duplicate-selection —
a JetBrains habit that clobbered VS Code's
`editor.action.addSelectionToNextFindMatch` (multi-cursor). VS Code
users now keep their muscle memory; JetBrains users opt in.
- Under the JetBrains preset, also wires up `Ctrl+Y` → delete line and
`Ctrl+Shift+/` → block comment.
- Drive-by tidy of the existing **Ctrl+S behaviour** dropdown in the
same tab so it's full-width and consistent with every other settings
dropdown (it had its `<select>` nested inside the `<label>` and used a
`mb-3` typo that rendered as an unknown `<mb-3>` element).

## Audit notes
I went through every `addAction` / `addCommand` in
`static/panes/{editor,compiler,ir-view,device-view}.ts` and only
`Ctrl+D` was a clear VS Code/JetBrains conflict. The other custom
bindings (`Ctrl+Enter`, `Ctrl+Shift+Enter`, `Ctrl+F8/F9/F10`,
`Ctrl+Shift+F1`) are CE-feature-specific and don't conflict with either
editor's defaults, so they're untouched.

Considered but skipped:
- `Alt+Up/Down` (move line) — already the Monaco/VS Code default;
nothing to add.
- `Ctrl+W` / `Ctrl+Shift+W` (extend/shrink selection in JB) —
browser-reserved, can't be intercepted.
- `Ctrl+B` (go-to-declaration in JB) — Firefox steals it for the
bookmarks sidebar, and CE doesn't run language servers for compiled
languages so it'd be a near no-op anyway.

## UX
The Keymap label reads "Keymap *(refresh required)*" with the
parenthetical in `<em>` so it inherits the theme's foreground colour (no
black-on-black on dark mode when selected). The binding decision is read
at editor construction so a page reload is needed to pick up changes —
consistent with how `autoIndent` / `enableCodeLens` already behave.

## Test plan
- [x] Open Settings → Keybindings, see the new Keymap dropdown defaulted
to "VS Code", full-width and visually consistent with the Ctrl+S
dropdown below it.
- [x] Switch theme to dark and confirm the "(refresh required)" hint is
readable (and doesn't disappear when selected).
- [x] In default (VS Code) mode: `Ctrl+D` adds the next find match
(multi-cursor); `Ctrl+Y` redoes; `Ctrl+Shift+/` does nothing custom.
- [x] Switch to JetBrains, refresh: `Ctrl+D` duplicates the selection;
`Ctrl+Y` deletes the current line; `Ctrl+Shift+/` toggles a block
comment.
- [x] Existing users with stored settings (no \`keymap\` key) get the VS
Code default and lose the previous unconditional `Ctrl+D` binding
(intended — switch to JetBrains to restore it).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: mattgodbolt-molty <mattgodbolt-molty@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:51:01 -04:00

168 lines
6.5 KiB
TypeScript

// Copyright (c) 2021, Compiler Explorer Authors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
import bigInt from 'big-integer';
import {addDigitSeparator} from '../shared/common-utils.js';
// Mirrors monaco-editor's own platform detection (vs/base/common/platform.js).
export const isMacintosh = typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Macintosh') >= 0;
/**
* Get an image relative to `/public` in the source root.
*/
export function getStaticImage(filename: string, parent?: string) {
const root = window.staticRoot;
if (parent) {
return `${root}${parent}/${filename}`;
}
return `${root}${filename}`;
}
export function updateAndCalcTopBarHeight(domRoot: JQuery, topBar: JQuery, hideable: JQuery): number {
let topBarHeight = 0;
if (!topBar.hasClass('d-none')) {
hideable.show();
const topBarHeightMax = topBar.outerHeight(true) ?? 0;
hideable.hide();
const topBarHeightMin = topBar.outerHeight(true) ?? 0;
topBarHeight = topBarHeightMin;
if (topBarHeightMin === topBarHeightMax) {
hideable.show();
}
}
return topBarHeight;
}
export function formatDateTimeWithSpaces(d: Date) {
const t = (x: string) => x.slice(-2);
// Hopefully some day we can use the temporal api to make this less of a pain
return (
`${d.getFullYear()} ${t('0' + (d.getMonth() + 1))} ${t('0' + d.getDate())}` +
`${t('0' + d.getHours())} ${t('0' + d.getMinutes())} ${t('0' + d.getSeconds())}`
);
}
export function formatISODate(dt: Date, full = false) {
const month = '' + (dt.getUTCMonth() + 1);
const day = '' + dt.getUTCDate();
const hrs = '' + dt.getUTCHours();
const min = '' + dt.getUTCMinutes();
const today = new Date(Date.now());
if (full || dt.toDateString() === today.toDateString()) {
return (
dt.getUTCFullYear() +
'-' +
month.padStart(2, '0') +
'-' +
day.padStart(2, '0') +
' ' +
hrs.padStart(2, '0') +
':' +
min.padStart(2, '0')
);
}
return dt.getUTCFullYear() + '-' + month.padStart(2, '0') + '-' + day.padStart(2, '0');
}
const hexLike = /^(#?[$]|0x)([0-9a-fA-F]+)$/;
const hexLike2 = /^(#?)([0-9a-fA-F]+)H$/;
const decimalLike = /^(#?)(-?[0-9]+)$/;
const ptxFloat32 = /^0[fF]([0-9a-fA-F]{8})$/;
const ptxFloat64 = /^0[dD]([0-9a-fA-F]{16})$/;
function parseNumericValue(value: string): bigInt.BigInteger | null {
const hexMatch = hexLike.exec(value) || hexLike2.exec(value);
if (hexMatch) return bigInt(hexMatch[2], 16);
const hexMatchPTX = ptxFloat32.exec(value) ?? ptxFloat64.exec(value);
if (hexMatchPTX) return bigInt(hexMatchPTX[1], 16);
const decMatch = decimalLike.exec(value);
if (decMatch) return bigInt(decMatch[2]);
return null;
}
export function getNumericToolTip(value: string, digitSeparator?: string): string | null {
const formatNumber = (num: bigInt.BigInteger, base: number, chunkSize: number) => {
const numberString = num.toString(base).toUpperCase();
if (digitSeparator !== undefined) {
return addDigitSeparator(numberString, digitSeparator, chunkSize);
}
return numberString;
};
const numericValue = parseNumericValue(value);
if (numericValue === null) return null;
// PTX floats
const view = new DataView(new ArrayBuffer(8));
view.setBigUint64(0, BigInt(numericValue.toString()), true);
if (ptxFloat32.test(value)) return view.getFloat32(0, true).toPrecision(9) + 'f';
if (ptxFloat64.test(value)) return view.getFloat64(0, true).toPrecision(17);
// Decimal representation.
let result = formatNumber(numericValue, 10, 3);
// Hexadecimal representation.
if (numericValue.isNegative()) {
const masked = bigInt('ffffffffffffffff', 16).and(numericValue);
result += ' = 0x' + formatNumber(masked, 16, 4);
} else {
result += ' = 0x' + formatNumber(numericValue, 16, 4);
}
// Float32/64 representation.
view.setBigUint64(0, BigInt(numericValue.toString()), true);
if (numericValue.bitLength().lesserOrEquals(32)) result += ' = ' + view.getFloat32(0, true).toPrecision(9) + 'f';
// only subnormal doubles and zero may have upper 32 bits all 0, assume unlikely to be double
else result += ' = ' + view.getFloat64(0, true).toPrecision(17);
// Printable UTF-8 characters.
const bytes = numericValue.isNegative()
? // bytes of negative number without sign extension
numericValue
.add(1)
.toArray(256)
.value.map(byte => byte ^ 0xff)
: numericValue.toArray(256).value;
// This assumes that `numericValue` is encoded as little-endian.
bytes.reverse();
const decoder = new TextDecoder('utf-8', {fatal: true});
try {
result += ' = ' + JSON.stringify(decoder.decode(Uint8Array.from(bytes)));
} catch {
// ignore `TypeError` when the number is not valid UTF-8
}
return result;
}
// zip two arrays up until min(a.length, b.length)
export function* zip<T>(a: T[], b: T[]) {
for (let i = 0; i < Math.min(a.length, b.length); i++) {
yield [a[i], b[i]] as [T, T];
}
}