Browse Source

Adds welcome style header to settings

Reorders settings
Removes views layout
main
Eric Amodio 1 year ago
parent
commit
264051c8a3
10 changed files with 802 additions and 871 deletions
  1. +1
    -1
      src/webviews/apps/settings/partials/commit-graph.html
  2. +43
    -41
      src/webviews/apps/settings/partials/rebase-editor.html
  3. +0
    -67
      src/webviews/apps/settings/partials/views.html
  4. +3
    -1
      src/webviews/apps/settings/partials/views.worktrees.html
  5. +55
    -59
      src/webviews/apps/settings/settings.html
  6. +46
    -7
      src/webviews/apps/settings/settings.scss
  7. +652
    -29
      src/webviews/apps/settings/settings.ts
  8. +0
    -666
      src/webviews/apps/shared/appWithConfigBase.ts
  9. +1
    -0
      src/webviews/settings/protocol.ts
  10. +1
    -0
      src/webviews/settings/settingsWebview.ts

+ 1
- 1
src/webviews/apps/settings/partials/commit-graph.html View File

@ -1,7 +1,7 @@
<section id="commit-graph" class="section--settings section--collapsible">
<div class="section__header">
<h2>
<span title="Requires a trial or subscription for use on privately hosted repos"></span> Commit Graph
Commit Graph&nbsp;<span title="Requires a trial or subscription for use on privately hosted repos"></span>
<a
class="link__learn-more"
title="Learn more"

+ 43
- 41
src/webviews/apps/settings/partials/rebase-editor.html View File

@ -23,59 +23,61 @@
</p>
</div>
<div class="section__group">
<div class="section__content">
<div class="settings settings--fixed ml-1">
<div class="setting" data-enablement="rebaseEditor.enabled" disabled>
<div class="setting__input setting__input--inner-select">
<label for="rebaseEditor.ordering">Show</label>
<div class="select-container">
<select id="rebaseEditor.ordering" name="rebaseEditor.ordering" data-setting>
<option value="asc">oldest commit first</option>
<option value="desc">newest commit first (default)</option>
</select>
<div class="section__collapsible">
<div class="section__group">
<div class="section__content">
<div class="settings settings--fixed ml-1">
<div class="setting" data-enablement="rebaseEditor.enabled" disabled>
<div class="setting__input setting__input--inner-select">
<label for="rebaseEditor.ordering">Show</label>
<div class="select-container">
<select id="rebaseEditor.ordering" name="rebaseEditor.ordering" data-setting>
<option value="asc">oldest commit first</option>
<option value="desc">newest commit first (default)</option>
</select>
</div>
</div>
</div>
</div>
<div class="setting" data-enablement="rebaseEditor.enabled" disabled>
<div class="setting__input setting__input--inner-select">
<input
id="rebaseEditor.showDetailsView"
name="rebaseEditor.showDetailsView"
type="checkbox"
value="selection"
data-setting
data-enablement="rebaseEditor.enabled"
disabled
/>
<label for="rebaseEditor.showDetailsView">Show Commit Details view</label>
<div class="select-container">
<select
<div class="setting" data-enablement="rebaseEditor.enabled" disabled>
<div class="setting__input setting__input--inner-select">
<input
id="rebaseEditor.showDetailsView"
name="rebaseEditor.showDetailsView"
type="checkbox"
value="selection"
data-setting
data-enablement="rebaseEditor.enabled &amp; rebaseEditor.showDetailsView !false"
data-enablement="rebaseEditor.enabled"
disabled
>
<option value="open">only when opening</option>
<option value="selection">when selection changes (default)</option>
</select>
/>
<label for="rebaseEditor.showDetailsView">Show Commit Details view</label>
<div class="select-container">
<select
id="rebaseEditor.showDetailsView"
name="rebaseEditor.showDetailsView"
data-setting
data-enablement="rebaseEditor.enabled &amp; rebaseEditor.showDetailsView !false"
disabled
>
<option value="open">only when opening</option>
<option value="selection">when selection changes (default)</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="section__preview">
<img
class="image__preview hidden"
src="#{webroot}/media/rebase-editor.webp"
data-visibility="rebaseEditor.enabled"
loading="lazy"
width="600"
height="291"
/>
<div class="section__preview">
<img
class="image__preview hidden"
src="#{webroot}/media/rebase-editor.webp"
data-visibility="rebaseEditor.enabled"
loading="lazy"
width="600"
height="291"
/>
</div>
</div>
</div>
</section>

+ 0
- 67
src/webviews/apps/settings/partials/views.html View File

@ -1,67 +0,0 @@
<section id="views" class="section--settings section--collapsible">
<div class="section__header">
<h2>
Views
<a
class="link__learn-more"
title="Learn more"
href="https://github.com/gitkraken/vscode-gitlens/#side-bar-views-"
>
<i class="icon icon__info"></i>
</a>
</h2>
<p class="section__header-hint">Adds rich views to visualize, navigate, and explore</p>
</div>
<div class="section__collapsible">
<div class="section__group">
<div class="section__content">
<p class="blurb mt-0">
GitLens views can be configured to be shown in different side bar layouts to best match your
workflow
</p>
<div class="presets mb-1">
<div class="preset">
<a
class="button button--flat"
title="Shows all the views together on the Source Control side bar"
href="command:gitlens.setViewsLayout?%7B%22layout%22%3A%22scm%22%7D"
>Source Control Layout (default)</a
>
<p>Shows all the views together on the Source Control side bar</p>
<img
class="image__preview"
src="#{webroot}/media/views-layout-scm.webp"
loading="lazy"
width="357"
height="410"
/>
</div>
<div class="preset">
<a
class="button button--flat"
title="Shows all the views together on the GitLens side bar"
href="command:gitlens.setViewsLayout?%7B%22layout%22%3A%22gitlens%22%7D"
>GitLens Layout</a
>
<p>Shows all the views together on the GitLens side bar</p>
<img
class="image__preview"
src="#{webroot}/media/views-layout-gitlens.webp"
loading="lazy"
width="357"
height="410"
/>
</div>
</div>
<p class="section__hint">
<i class="icon icon__info"></i> You can also simply drag &amp; drop individual views to create
custom layouts
</p>
</div>
</div>
</div>
</section>

+ 3
- 1
src/webviews/apps/settings/partials/views.worktrees.html View File

@ -1,7 +1,9 @@
<section id="worktrees-view" class="section--settings section--collapsible">
<div class="section__header">
<h2>
<span title="Requires a trial or subscription for use on privately hosted repos"></span> Worktrees view
Worktrees view&nbsp;<span title="Requires a trial or subscription for use on privately hosted repos"
>✨</span
>
<a
class="link__learn-more"
title="Learn more"

+ 55
- 59
src/webviews/apps/settings/settings.html View File

@ -15,19 +15,24 @@
<div class="container">
<header>
<a
class="header__link"
title="Learn more about GitLens"
href="https://gitkraken.com/gitlens?utm_source=gitlens-extension&utm_medium=in-app-links&utm_campaign=gitlens-logo-links"
>
<div class="header__logo">
<img class="image__logo" src="#{root}/images/gitlens-icon.png" />
<div>
<h1 class="header__title">Git<span class="header__title--highlight">Lens</span></h1>
<p class="header__subtitle">Git supercharged</p>
</div>
</div>
</a>
<h1 class="brand"><gitlens-logo></gitlens-logo> <small>Git Supercharged</small></h1>
<p class="release">
<span
>Version
<a
id="version"
href="https://github.com/gitkraken/vscode-gitlens/blob/main/CHANGELOG.md"
title="Open CHANGELOG"
aria-label="Open CHANGELOG"
></a
></span>
<a
href="https://help.gitkraken.com/gitlens/gitlens-release-notes-current"
title="Open Release Notes"
aria-label="Open Release Notes"
>Release notes</a
>
</p>
</header>
<div class="hero__area hero__area--sticky">
@ -66,7 +71,10 @@
<%= require('html-loader?{"esModule":false}!./partials/code-lens.html') %>
<%= require('html-loader?{"esModule":false}!./partials/status-bar.html') %>
<%= require('html-loader?{"esModule":false}!./partials/hovers.html') %>
<%= require('html-loader?{"esModule":false}!./partials/views.html') %>
<%= require('html-loader?{"esModule":false}!./partials/blame.html') %>
<%= require('html-loader?{"esModule":false}!./partials/changes.html') %>
<%= require('html-loader?{"esModule":false}!./partials/heatmap.html') %>
<%= require('html-loader?{"esModule":false}!./partials/commit-graph.html') %>
<%= require('html-loader?{"esModule":false}!./partials/views.commits.html') %>
<%= require('html-loader?{"esModule":false}!./partials/views.commitDetails.html') %>
<%= require('html-loader?{"esModule":false}!./partials/views.repositories.html') %>
@ -79,10 +87,6 @@
<%= require('html-loader?{"esModule":false}!./partials/views.worktrees.html') %>
<%= require('html-loader?{"esModule":false}!./partials/views.contributors.html') %>
<%= require('html-loader?{"esModule":false}!./partials/views.searchAndCompare.html') %>
<%= require('html-loader?{"esModule":false}!./partials/blame.html') %>
<%= require('html-loader?{"esModule":false}!./partials/changes.html') %>
<%= require('html-loader?{"esModule":false}!./partials/heatmap.html') %>
<%= require('html-loader?{"esModule":false}!./partials/commit-graph.html') %>
<%= require('html-loader?{"esModule":false}!./partials/rebase-editor.html') %>
<%= require('html-loader?{"esModule":false}!./partials/autolinks.html') %>
<%= require('html-loader?{"esModule":false}!./partials/terminal-links.html') %>
@ -137,15 +141,44 @@
<a
class="sidebar__jump-link"
data-action="jump"
href="#views"
title="Jump to Views settings"
>Views</a
href="#blame"
title="Jump to File Blame settings"
>File Blame</a
>
</li>
<li>
<a
class="sidebar__jump-link"
data-action="jump"
href="#changes"
title="Jump to File Changes settings"
>File Changes</a
>
</li>
<li>
<a
class="sidebar__jump-link"
data-action="jump"
href="#heatmap"
title="Jump to File Heatmap settings"
>File Heatmap</a
>
</li>
<li class="mt-1">
<a
class="sidebar__jump-link"
data-action="jump"
href="#commit-graph"
title="Jump to Commit Graph settings"
>Commit Graph ✨</a
>
</li>
<li class="mt-1">
<a
class="sidebar__jump-link"
data-action="jump"
href="#commits-view"
title="Jump to Commits view settings"
>Commits view</a
@ -229,7 +262,7 @@
data-action="jump"
href="#worktrees-view"
title="Jump to Worktrees view settings"
>Worktrees view</a
>Worktrees view</a
>
</li>
<li>
@ -255,43 +288,6 @@
<a
class="sidebar__jump-link"
data-action="jump"
href="#blame"
title="Jump to File Blame settings"
>File Blame</a
>
</li>
<li>
<a
class="sidebar__jump-link"
data-action="jump"
href="#changes"
title="Jump to File Changes settings"
>File Changes</a
>
</li>
<li>
<a
class="sidebar__jump-link"
data-action="jump"
href="#heatmap"
title="Jump to File Heatmap settings"
>File Heatmap</a
>
</li>
<li class="mt-1">
<a
class="sidebar__jump-link"
data-action="jump"
href="#commit-graph"
title="Jump to Commit Graph settings"
>✨ Commit Graph</a
>
</li>
<li>
<a
class="sidebar__jump-link"
data-action="jump"
href="#rebase-editor"
title="Jump to Interactive Rebase Editor settings"
>Interactive Rebase Editor</a

+ 46
- 7
src/webviews/apps/settings/settings.scss View File

@ -6,16 +6,55 @@ body {
&.vscode-light {
background-color: var(--color-background--darken-05);
}
font-family: var(--vscode-font-family);
font-weight: var(--vscode-font-weight);
font-size: var(--vscode-font-size) !important;
line-height: 1.4;
}
header {
grid-area: header;
display: grid;
grid-template-columns: auto;
grid-gap: 1rem;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
justify-items: center;
margin: 0 2em;
flex-wrap: wrap;
gap: 1.6rem;
grid-column: 1/-1;
& #version {
color: var(--color-foreground);
font-weight: 600;
}
& .brand {
margin: 0;
small {
display: inline-block;
font-size: 1.6rem;
font-weight: 200;
color: var(--color-foreground--50);
transform: translateY(0.3rem);
margin-left: 1rem;
}
}
& .release {
display: flex;
flex-direction: column;
align-items: flex-end;
margin: 0;
}
a:hover {
text-decoration: underline;
}
p,
li {
color: var(--color-foreground--65);
}
}
.blurb {
@ -167,7 +206,7 @@ header {
}
.hero__title {
font-size: 4rem;
font-size: 3rem;
margin: 0;
}

+ 652
- 29
src/webviews/apps/settings/settings.ts View File

@ -1,19 +1,37 @@
/*global document IntersectionObserver*/
import './settings.scss';
import type { AutolinkReference } from '../../../config';
import type { IpcMessage } from '../../protocol';
import {
DidChangeConfigurationNotificationType,
DidGenerateConfigurationPreviewNotificationType,
DidOpenAnchorNotificationType,
GenerateConfigurationPreviewCommandType,
onIpc,
UpdateConfigurationCommandType,
} from '../../protocol';
import type { State } from '../../settings/protocol';
import { AppWithConfig } from '../shared/appWithConfigBase';
import { App } from '../shared/appBase';
import { formatDate, setDefaultDateLocales } from '../shared/date';
import { DOM } from '../shared/dom';
// import { Snow } from '../shared/snow';
import '../welcome/components/gitlens-logo';
import '../welcome/components/gitlens-plus-logo';
const topOffset = 83;
const offset = (new Date().getTimezoneOffset() / 60) * 100;
const date = new Date(
`Wed Jul 25 2018 19:18:00 GMT${offset >= 0 ? '-' : '+'}${String(Math.abs(offset)).padStart(4, '0')}`,
);
export class SettingsApp extends AppWithConfig<State> {
export class SettingsApp extends App<State> {
private _scopes: HTMLSelectElement | null = null;
private _observer: IntersectionObserver | undefined;
private _activeSection: string | undefined = 'general';
private _changes = Object.create(null) as Record<string, any>;
private _sections = new Map<string, boolean>();
private _updating: boolean = false;
constructor() {
super('SettingsApp');
@ -66,21 +84,33 @@ export class SettingsApp extends AppWithConfig {
}
}
protected override beforeUpdateState() {
const focusId = document.activeElement?.id;
this.renderAutolinks();
if (focusId?.startsWith('autolinks.')) {
console.log(focusId, document.getElementById(focusId));
queueMicrotask(() => {
document.getElementById(focusId)?.focus();
});
}
}
protected override onBind() {
const disposables = super.onBind?.() ?? [];
disposables.push(
DOM.on('input[type=checkbox][data-setting]', 'change', (e, target: HTMLInputElement) =>
this.onInputChecked(target),
),
DOM.on(
'input[type=text][data-setting], input[type=number][data-setting], input:not([type])[data-setting]',
'blur',
(e, target: HTMLInputElement) => this.onInputBlurred(target),
),
DOM.on(
'input[type=text][data-setting], input[type=number][data-setting], input:not([type])[data-setting]',
'focus',
(e, target: HTMLInputElement) => this.onInputFocused(target),
),
DOM.on(
'input[type=text][data-setting][data-setting-preview], input[type=number][data-setting][data-setting-preview]',
'input',
(e, target: HTMLInputElement) => this.onInputChanged(target),
),
DOM.on('button[data-setting-clear]', 'click', (e, target: HTMLButtonElement) =>
this.onButtonClicked(target),
),
DOM.on('select[data-setting]', 'change', (e, target: HTMLSelectElement) => this.onInputSelected(target)),
DOM.on('.token[data-token]', 'mousedown', (e, target: HTMLElement) => this.onTokenMouseDown(target, e)),
DOM.on('.section--collapsible>.section__header', 'click', (e, target: HTMLInputElement) =>
this.onSectionHeaderClicked(target, e),
),
@ -104,14 +134,540 @@ export class SettingsApp extends AppWithConfig {
return disposables;
}
protected override scrollToAnchor(anchor: string, behavior: ScrollBehavior): void {
let offset = topOffset;
protected override onMessageReceived(e: MessageEvent) {
const msg = e.data as IpcMessage;
this.log(`onMessageReceived(${msg.id}): name=${msg.method}`);
switch (msg.method) {
case DidOpenAnchorNotificationType.method: {
onIpc(DidOpenAnchorNotificationType, msg, params => {
this.scrollToAnchor(params.anchor, params.scrollBehavior);
});
break;
}
case DidChangeConfigurationNotificationType.method:
onIpc(DidChangeConfigurationNotificationType, msg, params => {
this.state.config = params.config;
this.state.customSettings = params.customSettings;
this.updateState();
});
break;
default:
super.onMessageReceived?.(e);
}
}
private applyChanges() {
this.sendCommand(UpdateConfigurationCommandType, {
changes: { ...this._changes },
removes: Object.keys(this._changes).filter(k => this._changes[k] === undefined),
scope: this.getSettingsScope(),
});
this._changes = Object.create(null) as Record<string, any>;
}
private getSettingsScope(): 'user' | 'workspace' {
return this._scopes != null
? (this._scopes.options[this._scopes.selectedIndex].value as 'user' | 'workspace')
: 'user';
}
private onInputBlurred(element: HTMLInputElement) {
this.log(`onInputBlurred(${element.name}): value=${element.value})`);
const $popup = document.getElementById(`${element.name}.popup`);
if ($popup != null) {
$popup.classList.add('hidden');
}
let value: string | null | undefined = element.value;
if (value == null || value.length === 0) {
value = element.dataset.defaultValue;
if (value === undefined) {
value = null;
}
}
if (element.dataset.settingType === 'arrayObject') {
const props = element.name.split('.');
const settingName = props[0];
const index = parseInt(props[1], 10);
const objectProps = props.slice(2);
let setting: Record<string, any>[] | undefined = this.getSettingValue(settingName);
if (value == null && (setting === undefined || setting?.length === 0)) {
if (setting !== undefined) {
this._changes[settingName] = undefined;
}
} else {
setting = setting ?? [];
let settingItem = setting[index];
if (value != null || (value == null && settingItem !== undefined)) {
if (settingItem === undefined) {
settingItem = Object.create(null);
setting[index] = settingItem;
}
set(
settingItem,
objectProps.join('.'),
element.type === 'number' && value != null ? Number(value) : value,
);
this._changes[settingName] = setting;
}
}
} else {
this._changes[element.name] = element.type === 'number' && value != null ? Number(value) : value;
}
// this.setAdditionalSettings(element.checked ? element.dataset.addSettingsOn : element.dataset.addSettingsOff);
this.applyChanges();
}
private onButtonClicked(element: HTMLButtonElement) {
if (element.dataset.settingType === 'arrayObject') {
const props = element.name.split('.');
const settingName = props[0];
const setting = this.getSettingValue<Record<string, any>[]>(settingName);
if (setting === undefined) return;
const index = parseInt(props[1], 10);
if (setting[index] == null) return;
setting.splice(index, 1);
this._changes[settingName] = setting.length ? setting : undefined;
this.applyChanges();
}
}
private onInputChanged(element: HTMLInputElement) {
if (this._updating) return;
for (const el of document.querySelectorAll<HTMLSpanElement>(`span[data-setting-preview="${element.name}"]`)) {
this.updatePreview(el, element.value);
}
}
private onInputChecked(element: HTMLInputElement) {
if (this._updating) return;
this.log(`onInputChecked(${element.name}): checked=${element.checked}, value=${element.value})`);
switch (element.dataset.settingType) {
case 'object': {
const props = element.name.split('.');
const settingName = props.splice(0, 1)[0];
const setting = this.getSettingValue(settingName) ?? Object.create(null);
if (element.checked) {
set(setting, props.join('.'), fromCheckboxValue(element.value));
} else {
set(setting, props.join('.'), false);
}
this._changes[settingName] = setting;
break;
}
case 'array': {
const setting = this.getSettingValue(element.name) ?? [];
if (Array.isArray(setting)) {
if (element.checked) {
if (!setting.includes(element.value)) {
setting.push(element.value);
}
} else {
const i = setting.indexOf(element.value);
if (i !== -1) {
setting.splice(i, 1);
}
}
this._changes[element.name] = setting;
}
break;
}
case 'arrayObject': {
const props = element.name.split('.');
const settingName = props[0];
const index = parseInt(props[1], 10);
const objectProps = props.slice(2);
const setting: Record<string, any>[] = this.getSettingValue(settingName) ?? [];
const settingItem = setting[index] ?? Object.create(null);
if (setting[index] === undefined) {
setting[index] = settingItem;
}
if (element.checked) {
set(setting[index], objectProps.join('.'), fromCheckboxValue(element.value));
} else {
set(setting[index], objectProps.join('.'), false);
}
this._changes[settingName] = setting;
break;
}
case 'custom': {
this._changes[element.name] = element.checked;
break;
}
default: {
if (element.checked) {
this._changes[element.name] = fromCheckboxValue(element.value);
} else {
this._changes[element.name] = element.dataset.valueOff == null ? false : element.dataset.valueOff;
}
break;
}
}
this.setAdditionalSettings(element.checked ? element.dataset.addSettingsOn : element.dataset.addSettingsOff);
this.applyChanges();
}
private onInputFocused(element: HTMLInputElement) {
this.log(`onInputFocused(${element.name}): value=${element.value}`);
const $popup = document.getElementById(`${element.name}.popup`);
if ($popup != null) {
if ($popup.childElementCount === 0) {
const $template = document.querySelector<HTMLTemplateElement>('#token-popup')?.content.cloneNode(true);
if ($template != null) {
$popup.appendChild($template);
}
}
$popup.classList.remove('hidden');
}
}
private onInputSelected(element: HTMLSelectElement) {
if (element === this._scopes || this._updating) return;
const value = element.options[element.selectedIndex].value;
this.log(`onInputSelected(${element.name}): value=${value}`);
this._changes[element.name] = ensureIfBooleanOrNull(value);
this.applyChanges();
}
private onTokenMouseDown(element: HTMLElement, e: MouseEvent) {
if (this._updating) return;
this.log(`onTokenMouseDown(${element.id})`);
const setting = element.closest('.setting');
if (setting == null) return;
const input = setting.querySelector<HTMLInputElement>('input[type=text], input:not([type])');
if (input == null) return;
const token = `\${${element.dataset.token}}`;
let selectionStart = input.selectionStart;
if (selectionStart != null) {
input.value = `${input.value.substring(0, selectionStart)}${token}${input.value.substr(
input.selectionEnd ?? selectionStart,
)}`;
selectionStart += token.length;
} else {
selectionStart = input.value.length;
}
input.focus();
input.setSelectionRange(selectionStart, selectionStart);
if (selectionStart === input.value.length) {
input.scrollLeft = input.scrollWidth;
}
setTimeout(() => this.onInputChanged(input), 0);
setTimeout(() => input.focus(), 250);
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
}
private scrollToAnchor(anchor: string, behavior: ScrollBehavior, offset?: number) {
offset = topOffset;
const header = document.querySelector('.hero__area--sticky');
if (header != null) {
offset = header.clientHeight;
}
super.scrollToAnchor(anchor, behavior, offset);
const el = document.getElementById(anchor);
if (el == null) return;
this.scrollTo(el, behavior, offset);
}
private _scrollTimer: ReturnType<typeof setTimeout> | undefined;
private scrollTo(el: HTMLElement, behavior: ScrollBehavior, offset?: number) {
const top = el.getBoundingClientRect().top - document.body.getBoundingClientRect().top - (offset ?? 0);
window.scrollTo({
top: top,
behavior: behavior ?? 'smooth',
});
const fn = () => {
if (this._scrollTimer != null) {
clearTimeout(this._scrollTimer);
}
this._scrollTimer = setTimeout(() => {
window.removeEventListener('scroll', fn);
const newTop =
el.getBoundingClientRect().top - document.body.getBoundingClientRect().top - (offset ?? 0);
if (Math.abs(top - newTop) < 2) return;
this.scrollTo(el, behavior, offset);
}, 50);
};
window.addEventListener('scroll', fn, false);
}
private evaluateStateExpression(expression: string, changes: Record<string, string | boolean>): boolean {
let state = false;
for (const expr of expression.trim().split('&')) {
const [lhs, op, rhs] = parseStateExpression(expr);
switch (op) {
case '=': {
// Equals
let value = changes[lhs];
if (value === undefined) {
value = this.getSettingValue<string | boolean>(lhs) ?? false;
}
state = rhs !== undefined ? rhs === String(value) : Boolean(value);
break;
}
case '!': {
// Not equals
let value = changes[lhs];
if (value === undefined) {
value = this.getSettingValue<string | boolean>(lhs) ?? false;
}
state = rhs !== undefined ? rhs !== String(value) : !value;
break;
}
case '+': {
// Contains
if (rhs !== undefined) {
const setting = this.getSettingValue<string[]>(lhs);
state = setting !== undefined ? setting.includes(rhs.toString()) : false;
}
break;
}
}
if (!state) break;
}
return state;
}
private getCustomSettingValue(path: string): boolean | undefined {
return this.state.customSettings?.[path];
}
private getSettingValue<T>(path: string): T | undefined {
const customSetting = this.getCustomSettingValue(path);
if (customSetting != null) return customSetting as unknown as T;
return get<T>(this.state.config, path);
}
private updateState() {
const { version } = this.state;
document.getElementById('version')!.textContent = version;
const focusId = document.activeElement?.id;
this.renderAutolinks();
if (focusId?.startsWith('autolinks.')) {
console.log(focusId, document.getElementById(focusId));
queueMicrotask(() => {
document.getElementById(focusId)?.focus();
});
}
this._updating = true;
setDefaultDateLocales(this.state.config.defaultDateLocale);
try {
for (const el of document.querySelectorAll<HTMLInputElement>('input[type=checkbox][data-setting]')) {
if (el.dataset.settingType === 'custom') {
el.checked = this.getCustomSettingValue(el.name) ?? false;
} else if (el.dataset.settingType === 'array') {
el.checked = (this.getSettingValue<string[]>(el.name) ?? []).includes(el.value);
} else if (el.dataset.valueOff != null) {
const value = this.getSettingValue<string>(el.name);
el.checked = el.dataset.valueOff !== value;
} else {
el.checked = this.getSettingValue<boolean>(el.name) ?? false;
}
}
for (const el of document.querySelectorAll<HTMLInputElement>(
'input[type=text][data-setting], input[type=number][data-setting], input:not([type])[data-setting]',
)) {
el.value = this.getSettingValue<string>(el.name) ?? '';
}
for (const el of document.querySelectorAll<HTMLSelectElement>('select[data-setting]')) {
const value = this.getSettingValue<string>(el.name);
const option = el.querySelector<HTMLOptionElement>(`option[value='${value}']`);
if (option != null) {
option.selected = true;
}
}
for (const el of document.querySelectorAll<HTMLSpanElement>('span[data-setting-preview]')) {
this.updatePreview(el);
}
} finally {
this._updating = false;
}
const state = flatten(this.state.config);
if (this.state.customSettings != null) {
for (const [key, value] of Object.entries(this.state.customSettings)) {
state[key] = value;
}
}
this.setVisibility(state);
this.setEnablement(state);
}
private setAdditionalSettings(expression: string | undefined) {
if (!expression) return;
const addSettings = parseAdditionalSettingsExpression(expression);
for (const [s, v] of addSettings) {
this._changes[s] = v;
}
}
private setEnablement(state: Record<string, string | boolean>) {
for (const el of document.querySelectorAll<HTMLElement>('[data-enablement]')) {
const disabled = !this.evaluateStateExpression(el.dataset.enablement!, state);
if (disabled) {
el.setAttribute('disabled', '');
} else {
el.removeAttribute('disabled');
}
if (el.matches('input,select')) {
(el as HTMLInputElement | HTMLSelectElement).disabled = disabled;
} else {
const input = el.querySelector<HTMLInputElement | HTMLSelectElement>('input,select');
if (input == null) continue;
input.disabled = disabled;
}
}
}
private setVisibility(state: Record<string, string | boolean>) {
for (const el of document.querySelectorAll<HTMLElement>('[data-visibility]')) {
el.classList.toggle('hidden', !this.evaluateStateExpression(el.dataset.visibility!, state));
}
}
private updatePreview(el: HTMLSpanElement, value?: string) {
const previewType = el.dataset.settingPreviewType;
switch (previewType) {
case 'date': {
if (value === undefined) {
value = this.getSettingValue<string>(el.dataset.settingPreview!);
}
if (!value) {
value = el.dataset.settingPreviewDefault;
if (value == null) {
const lookup = el.dataset.settingPreviewDefaultLookup;
if (lookup != null) {
value = this.getSettingValue<string>(lookup);
}
}
}
el.innerText = value == null ? '' : formatDate(date, value, undefined, false);
break;
}
case 'date-locale': {
if (value === undefined) {
value = this.getSettingValue<string>(el.dataset.settingPreview!);
}
if (!value) {
value = undefined;
}
const format = this.getSettingValue<string>(el.dataset.settingPreviewDefault!) ?? 'MMMM Do, YYYY h:mma';
try {
el.innerText = formatDate(date, format, value, false);
} catch (ex) {
el.innerText = ex.message;
}
break;
}
case 'commit':
case 'commit-uncommitted': {
if (value === undefined) {
value = this.getSettingValue<string>(el.dataset.settingPreview!);
}
if (!value) {
value = el.dataset.settingPreviewDefault;
if (value == null) {
const lookup = el.dataset.settingPreviewDefaultLookup;
if (lookup != null) {
value = this.getSettingValue<string>(lookup);
}
}
}
if (value == null) {
el.innerText = '';
return;
}
void this.sendCommandWithCompletion(
GenerateConfigurationPreviewCommandType,
{
key: el.dataset.settingPreview!,
type: previewType,
format: value,
},
DidGenerateConfigurationPreviewNotificationType,
).then(params => {
el.innerText = params.preview ?? '';
});
break;
}
default:
break;
}
}
private onObserver(entries: IntersectionObserverEntry[], _observer: IntersectionObserver) {
@ -154,12 +710,6 @@ export class SettingsApp extends AppWithConfig {
this.toggleJumpLink(this._activeSection, true);
}
protected override getSettingsScope(): 'user' | 'workspace' {
return this._scopes != null
? (this._scopes.options[this._scopes.selectedIndex].value as 'user' | 'workspace')
: 'user';
}
private onActionLinkClicked(element: HTMLElement, e: MouseEvent) {
switch (element.dataset.action) {
case 'collapse':
@ -201,13 +751,7 @@ export class SettingsApp extends AppWithConfig {
e.stopPropagation();
}
protected override onInputSelected(element: HTMLSelectElement) {
if (element === this._scopes) return;
super.onInputSelected(element);
}
protected onJumpToLinkClicked(element: HTMLAnchorElement, e: MouseEvent) {
private onJumpToLinkClicked(element: HTMLAnchorElement, e: MouseEvent) {
const href = element.getAttribute('href');
if (href == null) return;
@ -347,5 +891,84 @@ export class SettingsApp extends AppWithConfig {
}
}
function ensureIfBooleanOrNull(value: string | boolean): string | boolean | null {
if (value === 'true') return true;
if (value === 'false') return false;
if (value === 'null') return null;
return value;
}
function get<T>(o: Record<string, any>, path: string): T | undefined {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return path.split('.').reduce((o = {}, key) => (o == null ? undefined : o[key]), o) as T;
}
function set(o: Record<string, any>, path: string, value: any): Record<string, any> {
const props = path.split('.');
const length = props.length;
const lastIndex = length - 1;
let index = -1;
let nested = o;
while (nested != null && ++index < length) {
const key = props[index];
let newValue = value;
if (index !== lastIndex) {
const objValue = nested[key];
newValue = typeof objValue === 'object' ? objValue : {};
}
nested[key] = newValue;
nested = nested[key];
}
return o;
}
function parseAdditionalSettingsExpression(expression: string): [string, string | boolean | null][] {
const settingsExpression = expression.trim().split(',');
return settingsExpression.map<[string, string | boolean | null]>(s => {
const [setting, value] = s.split('=');
return [setting, ensureIfBooleanOrNull(value)];
});
}
function parseStateExpression(expression: string): [string, string, string | boolean | undefined] {
const [lhs, op, rhs] = expression.trim().split(/([=+!])/);
return [lhs.trim(), op !== undefined ? op.trim() : '=', rhs !== undefined ? rhs.trim() : rhs];
}
function flatten(o: Record<string, any>, path?: string): Record<string, any> {
const results: Record<string, any> = {};
for (const key in o) {
const value = o[key];
if (Array.isArray(value)) continue;
if (typeof value === 'object') {
Object.assign(results, flatten(value, path === undefined ? key : `${path}.${key}`));
} else {
results[path === undefined ? key : `${path}.${key}`] = value;
}
}
return results;
}
function fromCheckboxValue(elementValue: unknown) {
switch (elementValue) {
case 'on':
return true;
case 'null':
return null;
case 'undefined':
return undefined;
default:
return elementValue;
}
}
new SettingsApp();
// requestAnimationFrame(() => new Snow());

+ 0
- 666
src/webviews/apps/shared/appWithConfigBase.ts View File

@ -1,666 +0,0 @@
/*global document*/
import type { Config } from '../../../config';
import type { IpcMessage } from '../../protocol';
import {
DidChangeConfigurationNotificationType,
DidGenerateConfigurationPreviewNotificationType,
DidOpenAnchorNotificationType,
GenerateConfigurationPreviewCommandType,
onIpc,
UpdateConfigurationCommandType,
} from '../../protocol';
import { App } from './appBase';
import { formatDate, setDefaultDateLocales } from './date';
import { DOM } from './dom';
const offset = (new Date().getTimezoneOffset() / 60) * 100;
const date = new Date(
`Wed Jul 25 2018 19:18:00 GMT${offset >= 0 ? '-' : '+'}${String(Math.abs(offset)).padStart(4, '0')}`,
);
interface AppStateWithConfig {
timestamp: number;
config: Config;
customSettings?: Record<string, boolean>;
}
export abstract class AppWithConfig<State extends AppStateWithConfig> extends App<State> {
private _changes = Object.create(null) as Record<string, any>;
private _updating: boolean = false;
protected override onInitialized() {
this.updateState();
}
protected override onBind() {
const disposables = super.onBind?.() ?? [];
disposables.push(
DOM.on('input[type=checkbox][data-setting]', 'change', (e, target: HTMLInputElement) =>
this.onInputChecked(target),
),
DOM.on(
'input[type=text][data-setting], input[type=number][data-setting], input:not([type])[data-setting]',
'blur',
(e, target: HTMLInputElement) => this.onInputBlurred(target),
),
DOM.on(
'input[type=text][data-setting], input[type=number][data-setting], input:not([type])[data-setting]',
'focus',
(e, target: HTMLInputElement) => this.onInputFocused(target),
),
DOM.on(
'input[type=text][data-setting][data-setting-preview], input[type=number][data-setting][data-setting-preview]',
'input',
(e, target: HTMLInputElement) => this.onInputChanged(target),
),
DOM.on('button[data-setting-clear]', 'click', (e, target: HTMLButtonElement) =>
this.onButtonClicked(target),
),
DOM.on('select[data-setting]', 'change', (e, target: HTMLSelectElement) => this.onInputSelected(target)),
DOM.on('.token[data-token]', 'mousedown', (e, target: HTMLElement) => this.onTokenMouseDown(target, e)),
);
return disposables;
}
protected override onMessageReceived(e: MessageEvent) {
const msg = e.data as IpcMessage;
this.log(`onMessageReceived(${msg.id}): name=${msg.method}`);
switch (msg.method) {
case DidOpenAnchorNotificationType.method: {
onIpc(DidOpenAnchorNotificationType, msg, params => {
this.scrollToAnchor(params.anchor, params.scrollBehavior);
});
break;
}
case DidChangeConfigurationNotificationType.method:
onIpc(DidChangeConfigurationNotificationType, msg, params => {
this.state.config = params.config;
this.state.customSettings = params.customSettings;
this.updateState();
});
break;
default:
super.onMessageReceived?.(e);
}
}
protected applyChanges() {
this.sendCommand(UpdateConfigurationCommandType, {
changes: { ...this._changes },
removes: Object.keys(this._changes).filter(k => this._changes[k] === undefined),
scope: this.getSettingsScope(),
});
this._changes = Object.create(null) as Record<string, any>;
}
protected getSettingsScope(): 'user' | 'workspace' {
return 'user';
}
protected onInputBlurred(element: HTMLInputElement) {
this.log(`onInputBlurred(${element.name}): value=${element.value})`);
const $popup = document.getElementById(`${element.name}.popup`);
if ($popup != null) {
$popup.classList.add('hidden');
}
let value: string | null | undefined = element.value;
if (value == null || value.length === 0) {
value = element.dataset.defaultValue;
if (value === undefined) {
value = null;
}
}
if (element.dataset.settingType === 'arrayObject') {
const props = element.name.split('.');
const settingName = props[0];
const index = parseInt(props[1], 10);
const objectProps = props.slice(2);
let setting: Record<string, any>[] | undefined = this.getSettingValue(settingName);
if (value == null && (setting === undefined || setting?.length === 0)) {
if (setting !== undefined) {
this._changes[settingName] = undefined;
}
} else {
setting = setting ?? [];
let settingItem = setting[index];
if (value != null || (value == null && settingItem !== undefined)) {
if (settingItem === undefined) {
settingItem = Object.create(null);
setting[index] = settingItem;
}
set(
settingItem,
objectProps.join('.'),
element.type === 'number' && value != null ? Number(value) : value,
);
this._changes[settingName] = setting;
}
}
} else {
this._changes[element.name] = element.type === 'number' && value != null ? Number(value) : value;
}
// this.setAdditionalSettings(element.checked ? element.dataset.addSettingsOn : element.dataset.addSettingsOff);
this.applyChanges();
}
protected onButtonClicked(element: HTMLButtonElement) {
if (element.dataset.settingType === 'arrayObject') {
const props = element.name.split('.');
const settingName = props[0];
const setting = this.getSettingValue<Record<string, any>[]>(settingName);
if (setting === undefined) return;
const index = parseInt(props[1], 10);
if (setting[index] == null) return;
setting.splice(index, 1);
this._changes[settingName] = setting.length ? setting : undefined;
this.applyChanges();
}
}
protected onInputChanged(element: HTMLInputElement) {
if (this._updating) return;
for (const el of document.querySelectorAll<HTMLSpanElement>(`span[data-setting-preview="${element.name}"]`)) {
this.updatePreview(el, element.value);
}
}
protected onInputChecked(element: HTMLInputElement) {
if (this._updating) return;
this.log(`onInputChecked(${element.name}): checked=${element.checked}, value=${element.value})`);
switch (element.dataset.settingType) {
case 'object': {
const props = element.name.split('.');
const settingName = props.splice(0, 1)[0];
const setting = this.getSettingValue(settingName) ?? Object.create(null);
if (element.checked) {
set(setting, props.join('.'), fromCheckboxValue(element.value));
} else {
set(setting, props.join('.'), false);
}
this._changes[settingName] = setting;
break;
}
case 'array': {
const setting = this.getSettingValue(element.name) ?? [];
if (Array.isArray(setting)) {
if (element.checked) {
if (!setting.includes(element.value)) {
setting.push(element.value);
}
} else {
const i = setting.indexOf(element.value);
if (i !== -1) {
setting.splice(i, 1);
}
}
this._changes[element.name] = setting;
}
break;
}
case 'arrayObject': {
const props = element.name.split('.');
const settingName = props[0];
const index = parseInt(props[1], 10);
const objectProps = props.slice(2);
const setting: Record<string, any>[] = this.getSettingValue(settingName) ?? [];
const settingItem = setting[index] ?? Object.create(null);
if (setting[index] === undefined) {
setting[index] = settingItem;
}
if (element.checked) {
set(setting[index], objectProps.join('.'), fromCheckboxValue(element.value));
} else {
set(setting[index], objectProps.join('.'), false);
}
this._changes[settingName] = setting;
break;
}
case 'custom': {
this._changes[element.name] = element.checked;
break;
}
default: {
if (element.checked) {
this._changes[element.name] = fromCheckboxValue(element.value);
} else {
this._changes[element.name] = element.dataset.valueOff == null ? false : element.dataset.valueOff;
}
break;
}
}
this.setAdditionalSettings(element.checked ? element.dataset.addSettingsOn : element.dataset.addSettingsOff);
this.applyChanges();
}
protected onInputFocused(element: HTMLInputElement) {
this.log(`onInputFocused(${element.name}): value=${element.value}`);
const $popup = document.getElementById(`${element.name}.popup`);
if ($popup != null) {
if ($popup.childElementCount === 0) {
const $template = document.querySelector<HTMLTemplateElement>('#token-popup')?.content.cloneNode(true);
if ($template != null) {
$popup.appendChild($template);
}
}
$popup.classList.remove('hidden');
}
}
protected onInputSelected(element: HTMLSelectElement) {
if (this._updating) return;
const value = element.options[element.selectedIndex].value;
this.log(`onInputSelected(${element.name}): value=${value}`);
this._changes[element.name] = ensureIfBooleanOrNull(value);
this.applyChanges();
}
protected onTokenMouseDown(element: HTMLElement, e: MouseEvent) {
if (this._updating) return;
this.log(`onTokenMouseDown(${element.id})`);
const setting = element.closest('.setting');
if (setting == null) return;
const input = setting.querySelector<HTMLInputElement>('input[type=text], input:not([type])');
if (input == null) return;
const token = `\${${element.dataset.token}}`;
let selectionStart = input.selectionStart;
if (selectionStart != null) {
input.value = `${input.value.substring(0, selectionStart)}${token}${input.value.substr(
input.selectionEnd ?? selectionStart,
)}`;
selectionStart += token.length;
} else {
selectionStart = input.value.length;
}
input.focus();
input.setSelectionRange(selectionStart, selectionStart);
if (selectionStart === input.value.length) {
input.scrollLeft = input.scrollWidth;
}
setTimeout(() => this.onInputChanged(input), 0);
setTimeout(() => input.focus(), 250);
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
}
protected scrollToAnchor(anchor: string, behavior: ScrollBehavior, offset?: number) {
const el = document.getElementById(anchor);
if (el == null) return;
this.scrollTo(el, behavior, offset);
}
private _scrollTimer: ReturnType<typeof setTimeout> | undefined;
private scrollTo(el: HTMLElement, behavior: ScrollBehavior, offset?: number) {
const top = el.getBoundingClientRect().top - document.body.getBoundingClientRect().top - (offset ?? 0);
window.scrollTo({
top: top,
behavior: behavior ?? 'smooth',
});
const fn = () => {
if (this._scrollTimer != null) {
clearTimeout(this._scrollTimer);
}
this._scrollTimer = setTimeout(() => {
window.removeEventListener('scroll', fn);
const newTop =
el.getBoundingClientRect().top - document.body.getBoundingClientRect().top - (offset ?? 0);
if (Math.abs(top - newTop) < 2) return;
this.scrollTo(el, behavior, offset);
}, 50);
};
window.addEventListener('scroll', fn, false);
}
private evaluateStateExpression(expression: string, changes: Record<string, string | boolean>): boolean {
let state = false;
for (const expr of expression.trim().split('&')) {
const [lhs, op, rhs] = parseStateExpression(expr);
switch (op) {
case '=': {
// Equals
let value = changes[lhs];
if (value === undefined) {
value = this.getSettingValue<string | boolean>(lhs) ?? false;
}
state = rhs !== undefined ? rhs === String(value) : Boolean(value);
break;
}
case '!': {
// Not equals
let value = changes[lhs];
if (value === undefined) {
value = this.getSettingValue<string | boolean>(lhs) ?? false;
}
state = rhs !== undefined ? rhs !== String(value) : !value;
break;
}
case '+': {
// Contains
if (rhs !== undefined) {
const setting = this.getSettingValue<string[]>(lhs);
state = setting !== undefined ? setting.includes(rhs.toString()) : false;
}
break;
}
}
if (!state) break;
}
return state;
}
private getCustomSettingValue(path: string): boolean | undefined {
return this.state.customSettings?.[path];
}
private getSettingValue<T>(path: string): T | undefined {
const customSetting = this.getCustomSettingValue(path);
if (customSetting != null) return customSetting as unknown as T;
return get<T>(this.state.config, path);
}
protected beforeUpdateState?(): void;
private updateState() {
this.beforeUpdateState?.();
this._updating = true;
setDefaultDateLocales(this.state.config.defaultDateLocale);
try {
for (const el of document.querySelectorAll<HTMLInputElement>('input[type=checkbox][data-setting]')) {
if (el.dataset.settingType === 'custom') {
el.checked = this.getCustomSettingValue(el.name) ?? false;
} else if (el.dataset.settingType === 'array') {
el.checked = (this.getSettingValue<string[]>(el.name) ?? []).includes(el.value);
} else if (el.dataset.valueOff != null) {
const value = this.getSettingValue<string>(el.name);
el.checked = el.dataset.valueOff !== value;
} else {
el.checked = this.getSettingValue<boolean>(el.name) ?? false;
}
}
for (const el of document.querySelectorAll<HTMLInputElement>(
'input[type=text][data-setting], input[type=number][data-setting], input:not([type])[data-setting]',
)) {
el.value = this.getSettingValue<string>(el.name) ?? '';
}
for (const el of document.querySelectorAll<HTMLSelectElement>('select[data-setting]')) {
const value = this.getSettingValue<string>(el.name);
const option = el.querySelector<HTMLOptionElement>(`option[value='${value}']`);
if (option != null) {
option.selected = true;
}
}
for (const el of document.querySelectorAll<HTMLSpanElement>('span[data-setting-preview]')) {
this.updatePreview(el);
}
} finally {
this._updating = false;
}
const state = flatten(this.state.config);
if (this.state.customSettings != null) {
for (const [key, value] of Object.entries(this.state.customSettings)) {
state[key] = value;
}
}
this.setVisibility(state);
this.setEnablement(state);
}
private setAdditionalSettings(expression: string | undefined) {
if (!expression) return;
const addSettings = parseAdditionalSettingsExpression(expression);
for (const [s, v] of addSettings) {
this._changes[s] = v;
}
}
private setEnablement(state: Record<string, string | boolean>) {
for (const el of document.querySelectorAll<HTMLElement>('[data-enablement]')) {
const disabled = !this.evaluateStateExpression(el.dataset.enablement!, state);
if (disabled) {
el.setAttribute('disabled', '');
} else {
el.removeAttribute('disabled');
}
if (el.matches('input,select')) {
(el as HTMLInputElement | HTMLSelectElement).disabled = disabled;
} else {
const input = el.querySelector<HTMLInputElement | HTMLSelectElement>('input,select');
if (input == null) continue;
input.disabled = disabled;
}
}
}
private setVisibility(state: Record<string, string | boolean>) {
for (const el of document.querySelectorAll<HTMLElement>('[data-visibility]')) {
el.classList.toggle('hidden', !this.evaluateStateExpression(el.dataset.visibility!, state));
}
}
private updatePreview(el: HTMLSpanElement, value?: string) {
const previewType = el.dataset.settingPreviewType;
switch (previewType) {
case 'date': {
if (value === undefined) {
value = this.getSettingValue<string>(el.dataset.settingPreview!);
}
if (!value) {
value = el.dataset.settingPreviewDefault;
if (value == null) {
const lookup = el.dataset.settingPreviewDefaultLookup;
if (lookup != null) {
value = this.getSettingValue<string>(lookup);
}
}
}
el.innerText = value == null ? '' : formatDate(date, value, undefined, false);
break;
}
case 'date-locale': {
if (value === undefined) {
value = this.getSettingValue<string>(el.dataset.settingPreview!);
}
if (!value) {
value = undefined;
}
const format = this.getSettingValue<string>(el.dataset.settingPreviewDefault!) ?? 'MMMM Do, YYYY h:mma';
try {
el.innerText = formatDate(date, format, value, false);
} catch (ex) {
el.innerText = ex.message;
}
break;
}
case 'commit':
case 'commit-uncommitted': {
if (value === undefined) {
value = this.getSettingValue<string>(el.dataset.settingPreview!);
}
if (!value) {
value = el.dataset.settingPreviewDefault;
if (value == null) {
const lookup = el.dataset.settingPreviewDefaultLookup;
if (lookup != null) {
value = this.getSettingValue<string>(lookup);
}
}
}
if (value == null) {
el.innerText = '';
return;
}
void this.sendCommandWithCompletion(
GenerateConfigurationPreviewCommandType,
{
key: el.dataset.settingPreview!,
type: previewType,
format: value,
},
DidGenerateConfigurationPreviewNotificationType,
).then(params => {
el.innerText = params.preview ?? '';
});
break;
}
default:
break;
}
}
}
function ensureIfBooleanOrNull(value: string | boolean): string | boolean | null {
if (value === 'true') return true;
if (value === 'false') return false;
if (value === 'null') return null;
return value;
}
function get<T>(o: Record<string, any>, path: string): T | undefined {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return path.split('.').reduce((o = {}, key) => (o == null ? undefined : o[key]), o) as T;
}
function set(o: Record<string, any>, path: string, value: any): Record<string, any> {
const props = path.split('.');
const length = props.length;
const lastIndex = length - 1;
let index = -1;
let nested = o;
while (nested != null && ++index < length) {
const key = props[index];
let newValue = value;
if (index !== lastIndex) {
const objValue = nested[key];
newValue = typeof objValue === 'object' ? objValue : {};
}
nested[key] = newValue;
nested = nested[key];
}
return o;
}
function parseAdditionalSettingsExpression(expression: string): [string, string | boolean | null][] {
const settingsExpression = expression.trim().split(',');
return settingsExpression.map<[string, string | boolean | null]>(s => {
const [setting, value] = s.split('=');
return [setting, ensureIfBooleanOrNull(value)];
});
}
function parseStateExpression(expression: string): [string, string, string | boolean | undefined] {
const [lhs, op, rhs] = expression.trim().split(/([=+!])/);
return [lhs.trim(), op !== undefined ? op.trim() : '=', rhs !== undefined ? rhs.trim() : rhs];
}
function flatten(o: Record<string, any>, path?: string): Record<string, any> {
const results: Record<string, any> = {};
for (const key in o) {
const value = o[key];
if (Array.isArray(value)) continue;
if (typeof value === 'object') {
Object.assign(results, flatten(value, path === undefined ? key : `${path}.${key}`));
} else {
results[path === undefined ? key : `${path}.${key}`] = value;
}
}
return results;
}
function fromCheckboxValue(elementValue: unknown) {
switch (elementValue) {
case 'on':
return true;
case 'null':
return null;
case 'undefined':
return undefined;
default:
return elementValue;
}
}

+ 1
- 0
src/webviews/settings/protocol.ts View File

@ -3,6 +3,7 @@ import type { Config } from '../../config';
export interface State {
timestamp: number;
version: string;
config: Config;
customSettings?: Record<string, boolean>;
scope: 'user' | 'workspace';

+ 1
- 0
src/webviews/settings/settingsWebview.ts View File

@ -41,6 +41,7 @@ export class SettingsWebviewProvider extends WebviewProviderWithConfigBase
return {
timestamp: Date.now(),
version: this.container.version,
// Make sure to get the raw config, not from the container which has the modes mixed in
config: configuration.getAll(true),
customSettings: this.getCustomSettings(),

Loading…
Cancel
Save