瀏覽代碼

Uses new webview for welcome and settings editor

Adds menu & toolbar settings to the settings editor
Adds mode setting for settings editor
Adds sticky header to settings editor
main
Eric Amodio 6 年之前
父節點
當前提交
acdc6e3fed
共有 15 個檔案被更改,包括 897 行新增399 行删除
  1. +10
    -0
      package.json
  2. +0
    -3
      src/configuration.ts
  3. +14
    -7
      src/container.ts
  4. +0
    -100
      src/pageProvider.ts
  5. +31
    -0
      src/ui/ipc.ts
  6. +26
    -3
      src/ui/scss/main.scss
  7. +38
    -4
      src/ui/settings/app.ts
  8. +186
    -72
      src/ui/settings/index.html
  9. +298
    -162
      src/ui/shared/app-base.ts
  10. +24
    -43
      src/ui/shared/colors.ts
  11. +21
    -4
      src/ui/welcome/app.ts
  12. +1
    -1
      src/ui/welcome/index.html
  13. +46
    -0
      src/webviews/settingsEditor.ts
  14. +167
    -0
      src/webviews/webviewEditor.ts
  15. +35
    -0
      src/webviews/welcomeEditor.ts

+ 10
- 0
package.json 查看文件

@ -829,6 +829,16 @@
"description": "Specifies when to switch between displaying files as a `tree` or `list` based on the number of files in a nesting level in the `GitLens Results` view\nOnly applies when displaying files as `auto`",
"scope": "window"
},
"gitlens.settings.mode": {
"type": "string",
"default": "simple",
"enum": [
"simple",
"advanced"
],
"description": "Specifies the display mode of the interactive settings editor\n `simple` - only displays common settings\n `advanced` - displays all settings",
"scope": "window"
},
"gitlens.showWhatsNewAfterUpgrades": {
"type": "boolean",
"default": true,

+ 0
- 3
src/configuration.ts 查看文件

@ -30,9 +30,6 @@ export class Configuration {
if (!e.affectsConfiguration(ExtensionKey, null!)) return;
Container.resetConfig();
if (Container.pages !== undefined) {
Container.pages.refresh();
}
if (configuration.changed(e, configuration.name('defaultGravatarsStyle').value)) {
clearGravatarCache();

+ 14
- 7
src/container.ts 查看文件

@ -14,9 +14,10 @@ import { GitRevisionCodeLensProvider } from './gitRevisionCodeLensProvider';
import { GitService } from './gitService';
import { HistoryExplorer } from './views/historyExplorer';
import { Keyboard } from './keyboard';
import { PageProvider } from './pageProvider';
import { ResultsExplorer } from './views/resultsExplorer';
import { SettingsEditor } from './webviews/settingsEditor';
import { StatusBarController } from './statusBarController';
import { WelcomeEditor } from './webviews/welcomeEditor';
export class Container {
@ -37,7 +38,8 @@ export class Container {
context.subscriptions.push(this._statusBarController = new StatusBarController());
context.subscriptions.push(this._codeLensController = new CodeLensController());
context.subscriptions.push(this._keyboard = new Keyboard());
context.subscriptions.push(this._pageProvider = new PageProvider());
context.subscriptions.push(this._settingsEditor = new SettingsEditor());
context.subscriptions.push(this._welcomeEditor = new WelcomeEditor());
if (config.gitExplorer.enabled) {
context.subscriptions.push(this._gitExplorer = new GitExplorer());
@ -139,11 +141,6 @@ export class Container {
return this._lineTracker;
}
private static _pageProvider: PageProvider;
static get pages() {
return this._pageProvider;
}
private static _resultsExplorer: ResultsExplorer | undefined;
static get resultsExplorer() {
if (this._resultsExplorer === undefined) {
@ -153,6 +150,11 @@ export class Container {
return this._resultsExplorer;
}
private static _settingsEditor: SettingsEditor;
static get settingsEditor() {
return this._settingsEditor;
}
private static _statusBarController: StatusBarController;
static get statusBar() {
return this._statusBarController;
@ -163,6 +165,11 @@ export class Container {
return this._tracker;
}
private static _welcomeEditor: WelcomeEditor;
static get welcomeEditor() {
return this._welcomeEditor;
}
static resetConfig() {
this._config = undefined;
}

+ 0
- 100
src/pageProvider.ts 查看文件

@ -1,100 +0,0 @@
'use strict';
import { commands, ConfigurationTarget, Disposable, Event, EventEmitter, TextDocument, TextDocumentContentProvider, Uri, ViewColumn, workspace } from 'vscode';
import { Container } from './container';
import { configuration } from './configuration';
import { Logger } from './logger';
const settingsUri = Uri.parse('gitlens://authority/settings');
const welcomeUri = Uri.parse('gitlens://authority/welcome');
export class PageProvider extends Disposable implements TextDocumentContentProvider {
private readonly _onDidChange = new EventEmitter<Uri>();
get onDidChange(): Event<Uri> {
return this._onDidChange.event;
}
private readonly _disposable: Disposable;
private _scope: Map<string, 'user' | 'workspace'> = new Map();
constructor() {
super(() => this.dispose());
this._disposable = Disposable.from(
workspace.onDidCloseTextDocument(this.onTextDocumentClosed, this),
workspace.registerTextDocumentContentProvider(settingsUri.scheme, this),
commands.registerCommand('gitlens.showSettingsPage', this.showSettings, this),
commands.registerCommand('gitlens.showWelcomePage', this.showWelcome, this),
commands.registerCommand('gitlens.saveSettings', this.save, this)
);
}
dispose() {
this._disposable.dispose();
}
private onTextDocumentClosed(e: TextDocument) {
this._scope.delete(e.uri.toString());
}
async provideTextDocumentContent(uri: Uri): Promise<string> {
const doc = await workspace.openTextDocument(Uri.file(Container.context.asAbsolutePath(`${uri.path}.html`)));
let text = doc.getText().replace(/{{root}}/g, Uri.file(Container.context.asAbsolutePath('.')).toString());
if (text.includes('\'{{data}}\'')) {
text = text.replace(/'{{data}}'/g, JSON.stringify({
config: Container.config,
scope: this.getScope(uri),
scopes: this.getAvailableScopes(),
uri: uri.toString()
}));
}
return text;
}
private getAvailableScopes(): ['user' | 'workspace', string][] {
const scopes: ['user' | 'workspace', string][] = [['user', 'User Settings']];
if (workspace.workspaceFolders !== undefined && workspace.workspaceFolders.length) {
scopes.push(['workspace', 'Workspace Settings']);
}
return scopes;
}
private getScope(uri: Uri): 'user' | 'workspace' {
return this._scope.get(uri.toString()) || 'user';
}
refresh(uri ?: Uri) {
Logger.log('PageProvider.refresh');
this._onDidChange.fire(uri || settingsUri);
}
async save(options: { changes: { [key: string]: any }, scope: 'user' | 'workspace', uri: string }) {
Logger.log(`PageProvider.save: options=${JSON.stringify(options)}`);
this._scope.set(options.uri, options.scope);
const target = options.scope === 'workspace'
? ConfigurationTarget.Workspace
: ConfigurationTarget.Global;
for (const key in options.changes) {
const inspect = await configuration.inspect(key)!;
if (inspect.defaultValue === options.changes[key]) {
await configuration.update(key, undefined, target);
}
else {
await configuration.update(key, options.changes[key], target);
}
}
}
async showSettings() {
return await commands.executeCommand('vscode.previewHtml', settingsUri, ViewColumn.Active, 'GitLens Settings');
}
async showWelcome() {
return await commands.executeCommand('vscode.previewHtml', welcomeUri, ViewColumn.Active, 'Welcome to GitLens');
}
}

+ 31
- 0
src/ui/ipc.ts 查看文件

@ -0,0 +1,31 @@
'use strict';
import { IConfig } from './config';
export interface Bootstrap {
config: IConfig;
}
export interface SettingsBootstrap extends Bootstrap {
scope: 'user' | 'workspace';
scopes: ['user' | 'workspace', string][];
}
export interface WelcomeBootstrap extends Bootstrap {
}
export interface SaveSettingsMessage {
type: 'saveSettings';
changes: {
[key: string]: any
};
removes: string[];
scope: 'user' | 'workspace';
uri: string;
}
export interface SettingsChangedMessage {
type: 'settingsChanged';
config: IConfig;
}
export type Message = SaveSettingsMessage | SettingsChangedMessage;

+ 26
- 3
src/ui/scss/main.scss 查看文件

@ -657,6 +657,15 @@ ul {
position: relative;
}
.page-header--sticky {
background: var(--background-color);
margin: 1em 0 1em 0;
padding-bottom: 1em;
position: sticky;
top: 0;
z-index: 1;
}
.page-header__title {
font-size: 3em;
margin: 0;
@ -741,6 +750,7 @@ ul {
.section-group__content {
flex: auto 1 1;
margin-bottom: 50%;
min-width: 0;
}
@ -749,6 +759,7 @@ ul {
margin: -1em 0 0 3em;
position: sticky;
top: 0;
z-index: 2;
li {
white-space: nowrap;
@ -791,7 +802,6 @@ ul {
.section-groups {
display: flex;
justify-content: space-around;
margin-bottom: 50%;
margin-top: -1em;
}
@ -810,15 +820,23 @@ ul {
}
}
.settings-group__header-hint {
color: var(--color--75);
font-weight: 200;
margin-top: -1em;
}
.settings-group__hint {
color: var(--color--75);
font-weight: 200;
margin-left: 2.3em;
text-indent: -2.75em;
}
.settings-group__hint--more {
display: block;
margin-left: 34px;
margin-top: 0.5em;
text-indent: initial;
}
.settings-group__setting {
@ -857,7 +875,8 @@ ul {
margin: -1em 0 1em 1em;
}
.settings-scope {
.settings-toolbar {
display: flex;
margin: 1.6em 185px 0 0;
position: absolute;
right: 0;
@ -873,6 +892,10 @@ ul {
}
}
.settings-scope {
flex: 1 1 auto;
}
.sidebar-group {
margin-top: 1em;
}

+ 38
- 4
src/ui/settings/app.ts 查看文件

@ -1,20 +1,54 @@
'use strict';
import { DOM } from './../shared/dom';
import { App } from '../shared/app-base';
import { SettingsBootstrap } from '../ipc';
export class SettingsApp extends App {
const bootstrap: SettingsBootstrap = (window as any).bootstrap;
export class SettingsApp extends App<SettingsBootstrap> {
private _scopes: HTMLSelectElement | null = null;
constructor() {
super('SettingsApp');
super('SettingsApp', bootstrap);
}
protected bind() {
super.bind();
protected onInitialize() {
// Add scopes if available
const scopes = DOM.getElementById<HTMLSelectElement>('scopes');
if (scopes && this.bootstrap.scopes.length > 1) {
for (const [scope, text] of this.bootstrap.scopes) {
const option = document.createElement('option');
option.value = scope;
option.innerHTML = text;
if (this.bootstrap.scope === scope) {
option.selected = true;
}
scopes.appendChild(option);
}
scopes.parentElement!.classList.remove('hidden');
this._scopes = scopes;
}
}
protected onBind() {
const onSectionHeaderClicked = this.onSectionHeaderClicked.bind(this);
DOM.listenAll('.section__header', 'click', function(this: HTMLInputElement) { return onSectionHeaderClicked(this, ...arguments); });
}
protected getSettingsScope(): 'user' | 'workspace' {
return this._scopes != null
? this._scopes.options[this._scopes.selectedIndex].value as 'user' | 'workspace'
: 'user';
}
protected onInputSelected(element: HTMLSelectElement) {
if (element === this._scopes) return;
return super.onInputSelected(element);
}
private onSectionHeaderClicked(element: HTMLElement, e: MouseEvent) {
if ((e.target as HTMLElement).matches('i.icon__info') ||
(e.target as HTMLElement).matches('a.link__learn-more')) return;

+ 186
- 72
src/ui/settings/index.html 查看文件

@ -28,16 +28,24 @@
</p>
</header>
<div class="page-header">
<div class="page-header page-header--sticky">
<h2 class="page-header__title">Settings</h2>
<p class="page-header__subtitle">For advanced customizations refer to the <a title="See the GitLens settings docs" href="https://github.com/eamodio/vscode-gitlens/#gitlens-settings">GitLens settings docs</a> and edit your
<a class="command" title="Open User Settings" href="command:workbench.action.openGlobalSettings">User Settings</a>
</p>
<div class="settings-scope hidden">
<label for="scopes">Save to</label>
<select id="scopes" name="scope">
</select>
<div class="settings-toolbar">
<div class="settings-scope">
<label for="scopes">Save to</label>
<select id="scopes" name="scope">
</select>
</div>
<div class="ml-1">
<select class="setting" id="settings.mode" name="settings.mode">
<option value="simple">Simple</option>
<option value="advanced">Advanced</option>
</select>
</div>
</div>
</div>
@ -97,6 +105,7 @@
<a class="command" title="Open Keyboard Shortcuts" href="command:workbench.action.openGlobalKeybindings">Keyboard Shortcuts</a> editor
</span>
<p class="settings-group__hint">
<i class="icon icon--lg icon__info"></i>
Search for
<b>
<i>gitlens</i>
@ -105,6 +114,118 @@
</p>
</div>
<h3 class="settings-group__header">Menus &amp; Toolbars</h3>
<div class="settings-group">
<p class="settings-group__header-hint">
Any changes to menu &amp; toolbar settings will require a <a class="command" title="Reload Window" href="command:workbench.action.reloadWindow">reload</a> before they will take effect
</p>
<div class="settings-group__setting nowrap">
<input class="setting" id="menus" name="menus" type="checkbox" value="undefined"/>
<label for="menus">Add commands to the built-in menus &amp; toolbars</label>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="menus">
<input class="setting" id="menus.editor" name="menus.editor" type="checkbox" value="undefined" data-type="object" disabled/>
<label for="menus.editor">Add commands to the editor context menu</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editor">
<input class="setting" id="menus.editor.compare" name="menus.editor.compare" type="checkbox" data-type="object" disabled/>
<label for="menus.editor.compare">Add comparison commands</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editor">
<input class="setting" id="menus.editor.remote" name="menus.editor.remote" type="checkbox" data-type="object" disabled/>
<label for="menus.editor.remote">Add open in remote commands</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editor">
<input class="setting" id="menus.editor.details" name="menus.editor.details" type="checkbox" data-type="object" disabled/>
<label for="menus.editor.details">Add commit details command</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editor">
<input class="setting" id="menus.editor.history" name="menus.editor.history" type="checkbox" data-type="object" disabled/>
<label for="menus.editor.history">Add file history command</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editor">
<input class="setting" id="menus.editor.blame" name="menus.editor.blame" type="checkbox" data-type="object" disabled/>
<label for="menus.editor.blame">Add blame command</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editor">
<input class="setting" id="menus.editor.clipboard" name="menus.editor.clipboard" type="checkbox" data-type="object" disabled/>
<label for="menus.editor.clipboard">Add copy to clipboard commands</label>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="menus">
<input class="setting" id="menus.editorTab" name="menus.editorTab" type="checkbox" value="undefined" data-type="object" disabled/>
<label for="menus.editorTab">Add commands to the editor tab context menu</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editorTab">
<input class="setting" id="menus.editorTab.compare" name="menus.editorTab.compare" type="checkbox" data-type="object" disabled/>
<label for="menus.editorTab.compare">Add comparison commands</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editorTab">
<input class="setting" id="menus.editorTab.history" name="menus.editorTab.history" type="checkbox" data-type="object" disabled/>
<label for="menus.editorTab.history">Add file history command</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editorTab">
<input class="setting" id="menus.editorTab.remote" name="menus.editorTab.remote" type="checkbox" data-type="object" disabled/>
<label for="menus.editorTab.remote">Add open in remote command</label>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="menus">
<input class="setting" id="menus.editorGroup" name="menus.editorGroup" type="checkbox" value="undefined" data-type="object" disabled/>
<label for="menus.editorGroup">Add commands to the editor group menu &amp; toolbar</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editorGroup">
<input class="setting" id="menus.editorGroup.compare" name="menus.editorGroup.compare" type="checkbox" data-type="object" disabled/>
<label for="menus.editorGroup.compare">Add comparison toolbar commands</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editorGroup">
<input class="setting" id="menus.editorGroup.blame" name="menus.editorGroup.blame" type="checkbox" data-type="object" disabled/>
<label for="menus.editorGroup.blame">Add blame toolbar command</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editorGroup">
<input class="setting" id="menus.editorGroup.remote" name="menus.editorGroup.remote" type="checkbox" data-type="object" disabled/>
<label for="menus.editorGroup.remote">Add open in remote command</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.editorGroup">
<input class="setting" id="menus.editorGroup.history" name="menus.editorGroup.history" type="checkbox" data-type="object" disabled/>
<label for="menus.editorGroup.history">Add file history command</label>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="menus">
<input class="setting" id="menus.explorer" name="menus.explorer" type="checkbox" value="undefined" data-type="object" disabled/>
<label for="menus.explorer">Add commands to the explorer context menu</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.explorer">
<input class="setting" id="menus.explorer.remote" name="menus.explorer.remote" type="checkbox" data-type="object" disabled/>
<label for="menus.explorer.remote">Add open in remote command</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.explorer">
<input class="setting" id="menus.explorer.history" name="menus.explorer.history" type="checkbox" data-type="object" disabled/>
<label for="menus.explorer.history">Add file history command</label>
</div>
<div class="settings-group__setting nowrap ml-4 hidden" data-visibility="settings.mode =advanced" data-enablement="menus.explorer">
<input class="setting" id="menus.explorer.compare" name="menus.explorer.compare" type="checkbox" data-type="object" disabled/>
<label for="menus.explorer.compare">Add comparison commands</label>
</div>
</div>
<p class="settings-group__hint">
<i class="icon icon--lg icon__info"></i>
For more advanced customizations open <a class="command" title="Open User Settings" href="command:workbench.action.openGlobalSettings">User Settings</a> and search for <b><i>gitlens</i></b>
@ -128,7 +249,7 @@
<label for="gitExplorer.enabled">Show the GitLens explorer</label>
</div>
<div class="settings-group__setting ml-2" data-enablement="gitExplorer.enabled &amp; historyExplorer.enabled =false" disabled>
<div class="settings-group__setting ml-2" data-enablement="gitExplorer.enabled &amp; historyExplorer.enabled =false">
<label for="gitExplorer.view">When opened, show the</label>
<select class="setting hidden" id="gitExplorer.view" name="gitExplorer.view" data-visibility="historyExplorer.enabled =false" disabled>
<option value="auto">last selected view</option>
@ -142,7 +263,7 @@
</select>
</div>
<div class="settings-group__setting ml-2" data-enablement="gitExplorer.enabled" disabled>
<div class="settings-group__setting ml-2 hidden" data-enablement="gitExplorer.enabled" data-visibility="settings.mode =advanced">
<label for="gitExplorer.branches.layout">Layout branches</label>
<select class="setting" id="gitExplorer.branches.layout" name="gitExplorer.branches.layout" disabled>
<option value="list">as a list</option>
@ -150,7 +271,7 @@
</select>
</div>
<div class="settings-group__setting ml-2" data-enablement="gitExplorer.enabled" disabled>
<div class="settings-group__setting ml-2 hidden" data-enablement="gitExplorer.enabled" data-visibility="settings.mode =advanced">
<label for="gitExplorer.files.layout">Layout files</label>
<select class="setting" id="gitExplorer.files.layout" name="gitExplorer.files.layout" disabled>
<option value="auto">automatically</option>
@ -158,20 +279,20 @@
<option value="tree">as a tree</option>
</select>
</div>
<p class="setting__hint ml-3 hidden" data-visibility="gitExplorer.files.layout =auto">Chooses the best layout based on the number of files at each nesting level</p>
<p class="setting__hint ml-3 hidden" data-visibility="gitExplorer.files.layout =auto &amp; settings.mode =advanced">Chooses the best layout based on the number of files at each nesting level</p>
<div class="settings-group__setting nowrap ml-2" data-enablement="gitExplorer.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-enablement="gitExplorer.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="gitExplorer.files.compact" name="gitExplorer.files.compact" type="checkbox" disabled/>
<label for="gitExplorer.files.compact">Use compact layout</label>
</div>
<p class="setting__hint ml-3">Compacts (flattens) unnecessary nesting when using a tree layouts</p>
<p class="setting__hint ml-3 hidden" data-visibility="settings.mode =advanced">Compacts (flattens) unnecessary nesting when using a tree layouts</p>
<div class="settings-group__setting nowrap ml-2" data-enablement="gitExplorer.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-enablement="gitExplorer.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="gitExplorer.avatars" name="explorers.avatars" type="checkbox" disabled/>
<label for="gitExplorer.avatars">Use author avatars icons</label>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="gitExplorer.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-enablement="gitExplorer.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="gitExplorer.autoRefresh" name="gitExplorer.autoRefresh" type="checkbox" disabled/>
<label for="gitExplorer.autoRefresh">Automatically refreshes when the file system or any repository changes</label>
</div>
@ -217,7 +338,7 @@
</div>
<p class="setting__hint hidden" data-visibility="gitExplorer.enabled &amp; historyExplorer.enabled =false">The current file history will be shown (docked) in the GitLens explorer</p>
<div class="settings-group__setting nowrap ml-2" data-enablement="historyExplorer.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-enablement="historyExplorer.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="historyExplorer.avatars" name="explorers.avatars" type="checkbox" disabled/>
<label for="historyExplorer.avatars">Use author avatars icons</label>
</div>
@ -233,7 +354,7 @@
</div>
</section>
<section id="gitlens-results">
<section id="gitlens-results" class="hidden" data-visibility="settings.mode =advanced">
<div class="section__header">
<h2 class="section__title">GitLens Results
<a class="link__learn-more" title="Learn more" href="https://github.com/eamodio/vscode-gitlens/#gitlens-results-view">
@ -294,12 +415,11 @@
<label for="codeLens.enabled">Show the authorship code lenses</label>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="codeLens.enabled" disabled>
<div class="settings-group__setting nowrap ml-2" data-enablement="codeLens.enabled">
<input class="setting" id="codeLens.recentChange.enabled" name="codeLens.recentChange.enabled" type="checkbox" disabled/>
<label for="codeLens.recentChange.enabled">Add the author and date of the most recent change for the file or code block</label>
</div>
<div class="settings-group__setting ml-4" data-enablement="codeLens.enabled &amp; codeLens.recentChange.enabled"
disabled>
<div class="settings-group__setting ml-4 hidden" data-enablement="codeLens.enabled &amp; codeLens.recentChange.enabled" data-visibility="settings.mode =advanced">
<label for="codeLens.recentChange.command">When clicked</label>
<select class="setting" id="codeLens.recentChange.command" name="codeLens.recentChange.command" disabled>
<option value="gitlens.toggleFileBlame">toggles the file blame annotations</option>
@ -311,12 +431,11 @@
</select>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="codeLens.enabled" disabled>
<div class="settings-group__setting nowrap ml-2" data-enablement="codeLens.enabled">
<input class="setting" id="codeLens.authors.enabled" name="codeLens.authors.enabled" type="checkbox" disabled/>
<label for="codeLens.authors.enabled">Add the number of authors and the most prominent author of the file or code block</label>
</div>
<div class="settings-group__setting ml-4" data-enablement="codeLens.enabled &amp; codeLens.authors.enabled"
disabled>
<div class="settings-group__setting ml-4 hidden" data-enablement="codeLens.enabled &amp; codeLens.authors.enabled" data-visibility="settings.mode =advanced">
<label for="codeLens.authors.command">When clicked</label>
<select class="setting" id="codeLens.authors.command" name="codeLens.authors.command" disabled>
<option value="gitlens.toggleFileBlame">toggles the file blame annotations</option>
@ -328,18 +447,18 @@
</select>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="codeLens.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-enablement="codeLens.enabled" data-visibility="settings.mode =advanced">
<label class="non-interactive">Add code lens to the following scopes</label>
</div>
<div class="settings-group__setting nowrap ml-4" data-enablement="codeLens.enabled" disabled>
<div class="settings-group__setting nowrap ml-4 hidden" data-enablement="codeLens.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="codeLens.scopes" name="codeLens.scopes" type="checkbox" value="document" data-type="array" disabled/>
<label for="codeLens.scopes">File scope &mdash; added at the top of the file</label>
</div>
<div class="settings-group__setting nowrap ml-4" data-enablement="codeLens.enabled" disabled>
<div class="settings-group__setting nowrap ml-4 hidden" data-enablement="codeLens.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="codeLens.scopes-1" name="codeLens.scopes" type="checkbox" value="containers" data-type="array" disabled/>
<label for="codeLens.scopes-1">Containers scope &mdash; added at the start of modules, classes, interfaces, etc</label>
</div>
<div class="settings-group__setting nowrap ml-4" data-enablement="codeLens.enabled" disabled>
<div class="settings-group__setting nowrap ml-4 hidden" data-enablement="codeLens.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="codeLens.scopes-2" name="codeLens.scopes" type="checkbox" value="blocks" data-type="array" disabled/>
<label for="codeLens.scopes-2">Block scope &mdash; added at the start of functions, methods, etc</label>
</div>
@ -385,7 +504,7 @@
<label for="currentLine.enabled">Show a blame annotation at the end of the current line</label>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="currentLine.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-enablement="currentLine.enabled" data-visibility="settings.mode =advanced">
<label for="currentLine.format">Annotation&nbsp;format</label>
<input class="setting" id="currentLine.format" name="currentLine.format" type="text"
placeholder="&dollar;&lbrace;authorAgoOrDate&rbrace; &bull; &dollar;&lbrace;message&rbrace;"
@ -412,11 +531,11 @@
</div>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="currentLine.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-enablement="currentLine.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="currentLine.scrollable" name="currentLine.scrollable" type="checkbox" disabled/>
<label for="currentLine.scrollable">Include the annotation when scrolling the editor horizontally</label>
</div>
<p class="setting__hint ml-3">When enabled the annotation can be scrolled into view when it is outside the viewport</p>
<p class="setting__hint ml-3 hidden" data-visibility="settings.mode =advanced">When enabled the annotation can be scrolled into view when it is outside the viewport</p>
</div>
<div class="section__preview">
@ -455,7 +574,7 @@
</select>
</div>
<div class="settings-group__setting nowrap">
<div class="settings-group__setting nowrap hidden" data-visibility="settings.mode =advanced">
<label for="blame.format">Annotation&nbsp;format</label>
<input class="setting" id="blame.format" name="blame.format" type="text"
placeholder="&dollar;&lbrace;message|40?&rbrace; &dollar;&lbrace;agoOrDate|14-&rbrace;"
@ -488,7 +607,7 @@
</div>
<p class="setting__hint">Quickly tell the age of a line &mdash; indicator ranges from bright yellow (newer) to dark brown (older)</p>
<div class="settings-group__setting ml-2" data-enablement="blame.heatmap.enabled" disabled>
<div class="settings-group__setting ml-2 hidden" data-enablement="blame.heatmap.enabled" data-visibility="settings.mode =advanced"disabled>
<label for="blame.heatmap.location">Position the heatmap on the</label>
<select class="setting" id="blame.heatmap.location" name="blame.heatmap.location" disabled>
<option value="left">left</option>
@ -496,31 +615,31 @@
</select>
</div>
<div class="settings-group__setting nowrap">
<div class="settings-group__setting nowrap hidden" data-visibility="settings.mode =advanced">
<input class="setting" id="blame.avatars" name="blame.avatars" type="checkbox"/>
<label for="blame.avatars">Add author avatars in the gutter</label>
</div>
<div class="settings-group__setting nowrap">
<div class="settings-group__setting nowrap hidden" data-visibility="settings.mode =advanced">
<input class="setting" id="blame.compact" name="blame.compact" type="checkbox"/>
<label for="blame.compact">Use compact view</label>
</div>
<p class="setting__hint">Compacts (deduplicates) matching adjacent blame annotations</p>
<p class="setting__hint hidden" data-visibility="settings.mode =advanced">Compacts (deduplicates) matching adjacent blame annotations</p>
<div class="settings-group__setting nowrap">
<input class="setting" id="blame.highlight.enabled" name="blame.highlight.enabled" type="checkbox"/>
<label for="blame.highlight.enabled">Highlight other lines changed with the current line</label>
<label for="blame.highlight.enabled">Highlight other lines changed by the current line's commit</label>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="blame.highlight.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-enablement="blame.highlight.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="blame.highlight.locations" name="blame.highlight.locations" type="checkbox" value="gutter" data-type="array" disabled/>
<label for="blame.highlight.locations">Add gutter highlight</label>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="blame.highlight.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-enablement="blame.highlight.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="blame.highlight.locations-1" name="blame.highlight.locations" type="checkbox" value="line" data-type="array" disabled/>
<label for="blame.highlight.locations-1">Add line highlight</label>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="blame.highlight.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-enablement="blame.highlight.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="blame.highlight.locations-2" name="blame.highlight.locations" type="checkbox" value="overview" data-type="array" disabled/>
<label for="blame.highlight.locations-2">Add scroll bar highlight</label>
</div>
@ -612,17 +731,17 @@
</div>
<div class="settings-group__setting">
<div class="settings-group__setting nowrap ml-2 hidden" data-visibility="currentLine.enabled" data-enablement="hovers.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-visibility="currentLine.enabled" data-enablement="hovers.enabled">
<input class="setting" id="hovers.currentLine.enabled" name="hovers.currentLine.enabled" type="checkbox" disabled/>
<label for="hovers.currentLine.enabled">Show hovers for the current line</label>
</div>
<div class="settings-group__setting nowrap ml-2 hidden" data-visibility="currentLine.enabled =false" data-enablement="hovers.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-visibility="currentLine.enabled =false" data-enablement="hovers.enabled">
<input class="setting" id="hovers.currentLine.enabled-1" name="hovers.currentLine.enabled" type="checkbox" data-add-settings-on="hovers.currentLine.over=line" disabled/>
<label for="hovers.currentLine.enabled-1">Show hovers for the current line</label>
</div>
<div class="settings-group__setting ml-4 hidden" data-visibility="currentLine.enabled" data-enablement="hovers.enabled &amp; hovers.currentLine.enabled" disabled>
<div class="settings-group__setting ml-4 hidden" data-visibility="currentLine.enabled &amp; settings.mode =advanced" data-enablement="hovers.enabled &amp; hovers.currentLine.enabled">
<label for="hovers.currentLine.over">Shown when over the</label>
<select class="setting" id="hovers.currentLine.over" name="hovers.currentLine.over" disabled>
<option value="annotation">annotation only</option>
@ -630,7 +749,7 @@
</select>
</div>
<div class="settings-group__setting ml-4 hidden" data-visibility="currentLine.enabled =false" data-enablement="hovers.enabled &amp; hovers.currentLine.enabled &amp; currentLine.enabled" disabled>
<div class="settings-group__setting ml-4 hidden" data-visibility="currentLine.enabled =false &amp; settings.mode =advanced" data-enablement="hovers.enabled &amp; hovers.currentLine.enabled &amp; currentLine.enabled">
<label for="hovers.currentLine.over">Shown when over the</label>
<select class="setting" id="hovers.currentLine.over" name="hovers.currentLine.over" disabled>
<option value="annotation">annotation only</option>
@ -638,14 +757,12 @@
</select>
</div>
<div class="settings-group__setting nowrap ml-4" data-enablement="hovers.enabled &amp; hovers.currentLine.enabled"
disabled>
<div class="settings-group__setting nowrap ml-4 hidden" data-enablement="hovers.enabled &amp; hovers.currentLine.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="hovers.currentLine.details" name="hovers.currentLine.details" type="checkbox" disabled/>
<label for="hovers.currentLine.details">Add blame details</label>
</div>
<div class="settings-group__setting nowrap ml-4" data-enablement="hovers.enabled &amp; hovers.currentLine.enabled"
disabled>
<div class="settings-group__setting nowrap ml-4 hidden" data-enablement="hovers.enabled &amp; hovers.currentLine.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="hovers.currentLine.changes" name="hovers.currentLine.changes" type="checkbox" disabled/>
<label for="hovers.currentLine.changes">Add changes (diff)</label>
</div>
@ -671,12 +788,12 @@
<div class="settings-group">
<div class="settings-group__setting">
<div class="settings-group__setting nowrap ml-2" data-enablement="hovers.enabled" disabled>
<div class="settings-group__setting nowrap ml-2" data-enablement="hovers.enabled">
<input class="setting" id="hovers.annotations.enabled" name="hovers.annotations.enabled" type="checkbox" disabled/>
<label for="hovers.annotations.enabled">Show hovers while annotating</label>
</div>
<div class="settings-group__setting nowrap ml-4" data-enablement="hovers.enabled &amp; hovers.annotations.enabled" disabled>
<div class="settings-group__setting nowrap ml-4 hidden" data-enablement="hovers.enabled &amp; hovers.annotations.enabled" data-visibility="settings.mode =advanced">
<label for="hovers.annotations.over">Shown when over the</label>
<select class="setting" id="hovers.annotations.over" name="hovers.annotations.over" disabled>
<option value="annotation">annotation only</option>
@ -684,14 +801,12 @@
</select>
</div>
<div class="settings-group__setting nowrap ml-4" data-enablement="hovers.enabled &amp; hovers.annotations.enabled"
disabled>
<div class="settings-group__setting nowrap ml-4 hidden" data-enablement="hovers.enabled &amp; hovers.annotations.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="hovers.annotations.details" name="hovers.annotations.details" type="checkbox" disabled/>
<label for="hovers.annotations.details">Add blame details</label>
</div>
<div class="settings-group__setting nowrap ml-4" data-enablement="hovers.enabled &amp; hovers.annotations.enabled"
disabled>
<div class="settings-group__setting nowrap ml-4 hidden" data-enablement="hovers.enabled &amp; hovers.annotations.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="hovers.annotations.changes" name="hovers.annotations.changes" type="checkbox" disabled/>
<label for="hovers.annotations.changes">Add changes (diff)</label>
</div>
@ -729,17 +844,17 @@
</select>
</div>
<div class="settings-group__setting nowrap">
<div class="settings-group__setting nowrap hidden" data-visibility="settings.mode =advanced">
<input class="setting" id="recentChanges.highlight.locations" name="recentChanges.highlight.locations" type="checkbox" value="gutter" data-type="array"/>
<label for="recentChanges.highlight.locations">Add gutter highlight</label>
</div>
<div class="settings-group__setting nowrap">
<div class="settings-group__setting nowrap hidden" data-visibility="settings.mode =advanced">
<input class="setting" id="recentChanges.highlight.locations-1" name="recentChanges.highlight.locations" type="checkbox" value="line" data-type="array"/>
<label for="recentChanges.highlight.locations-1">Add line highlight</label>
</div>
<div class="settings-group__setting nowrap">
<div class="settings-group__setting nowrap hidden" data-visibility="settings.mode =advanced">
<input class="setting" id="recentChanges.highlight.locations-2" name="recentChanges.highlight.locations" type="checkbox" value="overview" data-type="array"/>
<label for="recentChanges.highlight.locations-2">Add scroll bar highlight</label>
</div>
@ -782,7 +897,7 @@
<label for="statusBar.enabled">Show a Git blame annotation for the current line in the status bar</label>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="statusBar.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-enablement="statusBar.enabled" data-visibility="settings.mode =advanced">
<label for="statusBar.format">Annotation&nbsp;format</label>
<input class="setting" id="statusBar.format" name="statusBar.format" type="text"
placeholder="&dollar;&lbrace;authorAgoOrDate&rbrace;"
@ -809,7 +924,7 @@
</div>
</div>
<div class="settings-group__setting ml-2" data-enablement="statusBar.enabled" disabled>
<div class="settings-group__setting ml-2" data-enablement="statusBar.enabled">
<label for="statusBar.alignment">Position the annotation on the</label>
<select class="setting" id="statusBar.alignment" name="statusBar.alignment" disabled>
<option value="left">left</option>
@ -817,7 +932,7 @@
</select>
</div>
<div class="settings-group__setting ml-2" data-enablement="statusBar.enabled" disabled>
<div class="settings-group__setting ml-2 hidden" data-enablement="statusBar.enabled" data-visibility="settings.mode =advanced">
<label for="statusBar.command">When clicked</label>
<select class="setting" id="statusBar.command" name="statusBar.command" disabled>
<option value="gitlens.toggleFileBlame">toggles the file blame annotations</option>
@ -831,11 +946,11 @@
</select>
</div>
<div class="settings-group__setting nowrap ml-2" data-enablement="statusBar.enabled" disabled>
<div class="settings-group__setting nowrap ml-2 hidden" data-enablement="statusBar.enabled" data-visibility="settings.mode =advanced">
<input class="setting" id="statusBar.reduceFlicker" name="statusBar.reduceFlicker" type="checkbox" disabled/>
<label for="statusBar.reduceFlicker">Reduce flashing when updating the annotation</label>
</div>
<p class="setting__hint ml-3">Avoids clearing the previous blame information when changing lines to reduce status bar "flashing"</p>
<p class="setting__hint ml-3 hidden" data-visibility="settings.mode =advanced">Avoids clearing the previous blame information when changing lines to reduce status bar "flashing"</p>
</div>
<div class="section__preview">
@ -860,17 +975,17 @@
<div class="sidebar-group">
<h2>Jump to</h2>
<ul>
<li><a href="#general">General</a></li>
<li><a href="#gitlens-explorer">GitLens Explorer</a></li>
<li><a href="#gitlens-history-explorer">GitLens History Explorer</a></li>
<li><a href="#gitlens-results">GitLens Results</a></li>
<li><a href="#code-lens">Code Lens</a></li>
<li><a href="#current-line">Current Line Blame</a></li>
<li><a href="#blame">Gutter Blame</a></li>
<li><a href="#heatmap">Gutter Heatmap</a></li>
<li><a href="#hovers">Hovers</a></li>
<li><a href="#recent-changes">Recent Changes</a></li>
<li><a href="#status-bar">Status Bar Blame</a></li>
<li><a class="jump-to" href="#general">General</a></li>
<li><a class="jump-to" href="#gitlens-explorer">GitLens Explorer</a></li>
<li><a class="jump-to" href="#gitlens-history-explorer">GitLens History Explorer</a></li>
<li><a class="jump-to" href="#gitlens-results" class="hidden" data-visibility="settings.mode =advanced">GitLens Results</a></li>
<li><a class="jump-to" href="#code-lens">Code Lens</a></li>
<li><a class="jump-to" href="#current-line">Current Line Blame</a></li>
<li><a class="jump-to" href="#blame">Gutter Blame</a></li>
<li><a class="jump-to" href="#heatmap">Gutter Heatmap</a></li>
<li><a class="jump-to" href="#hovers">Hovers</a></li>
<li><a class="jump-to" href="#recent-changes">Recent Changes</a></li>
<li><a class="jump-to" href="#status-bar">Status Bar Blame</a></li>
</ul>
</div>
<div class="sidebar-group">
@ -929,9 +1044,8 @@
</div>
</div>
<a id="commandRelay" style="display: none"></a>
<script type="text/javascript">
window.gitlens = '{{data}}';
window.bootstrap = '{{bootstrap}}';
</script>
</body>

+ 298
- 162
src/ui/shared/app-base.ts 查看文件

@ -1,39 +1,31 @@
'use strict';
import { DOM } from './../shared/dom';
import { initializeColorPalette } from '../shared/colors';
import { IConfig } from './../config';
import { darken, lighten, opacity } from '../shared/colors';
import { Bootstrap, Message, SaveSettingsMessage } from './../ipc';
const gitlens: { config: IConfig, scope: 'user' | 'workspace', scopes: ['user' | 'workspace', string][], uri: string } = (window as any).gitlens;
export abstract class App {
interface VsCodeApi {
postMessage(msg: {}): void;
setState(state: {}): void;
getState(): {};
}
private readonly _commandRelay: HTMLAnchorElement;
private readonly _changes: { [key: string]: any } = Object.create(null);
private readonly _scopes: HTMLSelectElement | null = null;
declare function acquireVsCodeApi(): VsCodeApi;
constructor(private _appName: string) {
this.log(`${this._appName}.ctor`);
export abstract class App<TBootstrap extends Bootstrap> {
this._commandRelay = DOM.getElementById<HTMLAnchorElement>('commandRelay');
private readonly _api: VsCodeApi;
private _changes: { [key: string]: any } = Object.create(null);
private _updating: boolean = false;
// Add scopes if available
const scopes = DOM.getElementById<HTMLSelectElement>('scopes');
if (scopes && gitlens.scopes.length > 1) {
for (const [scope, text] of gitlens.scopes) {
const option = document.createElement('option');
option.value = scope;
option.innerHTML = text;
if (gitlens.scope === scope) {
option.selected = true;
}
scopes.appendChild(option);
}
constructor(
protected readonly appName: string,
protected readonly bootstrap: TBootstrap
) {
this.log(`${this.appName}.ctor`);
scopes.parentElement!.classList.remove('hidden');
this._scopes = scopes;
}
this._api = acquireVsCodeApi();
initializeColorPalette();
this.initializeColorPalette();
this.initialize();
this.bind();
@ -42,59 +34,31 @@ export abstract class App {
}, 500);
}
protected initialize() {
this.log(`${this._appName}.initializeState`);
protected applyChanges() {
this.postMessage({
type: 'saveSettings',
changes: { ...this._changes },
removes: Object.keys(this._changes).filter(k => this._changes[k] === undefined),
scope: this.getSettingsScope()
for (const el of document.querySelectorAll<HTMLInputElement>('input[type=checkbox].setting')) {
const checked = el.dataset.type === 'array'
? (getSettingValue<string[]>(el.name) || []).includes(el.value)
: getSettingValue<boolean>(el.name) || false;
el.checked = checked;
}
} as SaveSettingsMessage);
for (const el of document.querySelectorAll<HTMLInputElement>('input[type=text].setting, input:not([type]).setting')) {
el.value = getSettingValue<string>(el.name) || '';
}
for (const el of document.querySelectorAll<HTMLSelectElement>('select.setting')) {
const value = getSettingValue<string>(el.name);
const option = el.querySelector<HTMLOptionElement>(`option[value='${value}']`);
if (option != null) {
option.selected = true;
}
}
const state = flatten(gitlens.config);
this.setVisibility(state);
this.setEnablement(state);
this._changes = Object.create(null);
}
protected bind() {
const onInputChecked = this.onInputChecked.bind(this);
DOM.listenAll('input[type=checkbox].setting', 'change', function(this: HTMLInputElement) { return onInputChecked(this, ...arguments); });
const onInputBlurred = this.onInputBlurred.bind(this);
DOM.listenAll('input[type=text].setting, input:not([type]).setting', 'blur', function(this: HTMLInputElement) { return onInputBlurred(this, ...arguments); });
const onInputFocused = this.onInputFocused.bind(this);
DOM.listenAll('input[type=text].setting, input:not([type]).setting', 'focus', function(this: HTMLInputElement) { return onInputFocused(this, ...arguments); });
const onInputSelected = this.onInputSelected.bind(this);
DOM.listenAll('select.setting', 'change', function(this: HTMLInputElement) { return onInputSelected(this, ...arguments); });
const onTokenMouseDown = this.onTokenMouseDown.bind(this);
DOM.listenAll('[data-token]', 'mousedown', function(this: HTMLElement) { return onTokenMouseDown(this, ...arguments); });
const onPopupMouseDown = this.onPopupMouseDown.bind(this);
DOM.listenAll('.popup', 'mousedown', function(this: HTMLElement) { return onPopupMouseDown(this, ...arguments); });
protected getSettingsScope(): 'user' | 'workspace' {
return 'user';
}
protected log(message: string) {
console.log(message);
}
private onInputBlurred(element: HTMLInputElement) {
this.log(`${this._appName}.onInputBlurred: name=${element.name}, value=${element.value}`);
protected onBind() { }
protected onInitialize() { }
protected onInputBlurred(element: HTMLInputElement) {
this.log(`${this.appName}.onInputBlurred: name=${element.name}, value=${element.value}`);
const popup = document.getElementById(`${element.name}.popup`);
if (popup != null) {
@ -115,32 +79,56 @@ export abstract class App {
this.applyChanges();
}
private onInputChecked(element: HTMLInputElement) {
this.log(`${this._appName}.onInputChecked: name=${element.name}, checked=${element.checked}, value=${element.value}`);
protected onInputChecked(element: HTMLInputElement) {
if (this._updating) return;
this.log(`${this.appName}.onInputChecked: name=${element.name}, checked=${element.checked}, value=${element.value}`);
switch (element.dataset.type) {
case 'object': {
const props = element.name.split('.');
const settingName = props.splice(0, 1)[0];
const setting = this.getSettingValue(settingName) || Object.create(null);
if (element.dataset.type === 'array') {
const setting = getSettingValue(element.name) || [];
if (Array.isArray(setting)) {
if (element.checked) {
if (!setting.includes(element.value)) {
setting.push(element.value);
}
set(setting, props.join('.'), fromCheckboxValue(element.value));
}
else {
const i = setting.indexOf(element.value);
if (i !== -1) {
setting.splice(i, 1);
}
set(setting, props.join('.'), false);
}
this._changes[element.name] = setting;
this._changes[settingName] = setting;
break;
}
}
else {
if (element.checked) {
this._changes[element.name] = element.value === 'on' ? true : element.value;
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;
}
else {
this._changes[element.name] = false;
default: {
if (element.checked) {
this._changes[element.name] = fromCheckboxValue(element.value);
}
else {
this._changes[element.name] = false;
}
break;
}
}
@ -148,8 +136,8 @@ export abstract class App {
this.applyChanges();
}
private onInputFocused(element: HTMLInputElement) {
this.log(`${this._appName}.onInputFocused: name=${element.name}, value=${element.value}`);
protected onInputFocused(element: HTMLInputElement) {
this.log(`${this.appName}.onInputFocused: name=${element.name}, value=${element.value}`);
const popup = document.getElementById(`${element.name}.popup`);
if (popup != null) {
@ -157,26 +145,63 @@ export abstract class App {
}
}
private onInputSelected(element: HTMLSelectElement) {
if (element === this._scopes) return;
protected onInputSelected(element: HTMLSelectElement) {
if (this._updating) return;
const value = element.options[element.selectedIndex].value;
this.log(`${this._appName}.onInputSelected: name=${element.name}, value=${value}`);
this.log(`${this.appName}.onInputSelected: name=${element.name}, value=${value}`);
this._changes[element.name] = ensureIfBoolean(value);
this.applyChanges();
}
private onPopupMouseDown(element: HTMLElement, e: MouseEvent) {
protected onJumpToLinkClicked(element: HTMLAnchorElement, e: MouseEvent) {
const href = element.getAttribute('href');
if (href == null) return;
const el = document.getElementById(href.substr(1));
if (el == null) return;
let height = 83;
const header = document.querySelector('.page-header--sticky');
if (header != null) {
height = header.clientHeight;
}
el.scrollIntoView({
block: 'start',
behavior: 'instant'
});
window.scrollBy(0, -height);
e.stopPropagation();
e.preventDefault();
}
protected onMessageReceived(e: MessageEvent) {
const msg = e.data as Message;
switch (msg.type) {
case 'settingsChanged':
this.bootstrap.config = msg.config;
this.setState();
break;
}
}
protected onPopupMouseDown(element: HTMLElement, e: MouseEvent) {
// e.stopPropagation();
// e.stopImmediatePropagation();
e.preventDefault();
}
private onTokenMouseDown(element: HTMLElement, e: MouseEvent) {
this.log(`${this._appName}.onTokenClicked: id=${element.id}`);
protected onTokenMouseDown(element: HTMLElement, e: MouseEvent) {
if (this._updating) return;
this.log(`${this.appName}.onTokenClicked: id=${element.id}`);
const setting = element.closest('.settings-group__setting');
if (setting == null) return;
@ -191,31 +216,147 @@ export abstract class App {
e.preventDefault();
}
private applyChanges() {
const args = JSON.stringify({
changes: this._changes,
scope: this.getScope(),
uri: gitlens.uri
});
this.log(`${this._appName}.applyChanges: args=${args}`);
protected postMessage(e: Message) {
this._api.postMessage(e);
}
private bind() {
this.onBind();
const onMessageReceived = this.onMessageReceived.bind(this);
window.addEventListener('message', onMessageReceived);
const command = 'command:gitlens.saveSettings?' + encodeURI(args);
setTimeout(() => this.executeCommand(command), 0);
const onInputChecked = this.onInputChecked.bind(this);
DOM.listenAll('input[type=checkbox].setting', 'change', function(this: HTMLInputElement) { return onInputChecked(this, ...arguments); });
const onInputBlurred = this.onInputBlurred.bind(this);
DOM.listenAll('input[type=text].setting, input:not([type]).setting', 'blur', function(this: HTMLInputElement) { return onInputBlurred(this, ...arguments); });
const onInputFocused = this.onInputFocused.bind(this);
DOM.listenAll('input[type=text].setting, input:not([type]).setting', 'focus', function(this: HTMLInputElement) { return onInputFocused(this, ...arguments); });
const onInputSelected = this.onInputSelected.bind(this);
DOM.listenAll('select.setting', 'change', function(this: HTMLInputElement) { return onInputSelected(this, ...arguments); });
const onTokenMouseDown = this.onTokenMouseDown.bind(this);
DOM.listenAll('[data-token]', 'mousedown', function(this: HTMLElement) { return onTokenMouseDown(this, ...arguments); });
const onPopupMouseDown = this.onPopupMouseDown.bind(this);
DOM.listenAll('.popup', 'mousedown', function(this: HTMLElement) { return onPopupMouseDown(this, ...arguments); });
const onJumpToLinkClicked = this.onJumpToLinkClicked.bind(this);
DOM.listenAll('a.jump-to[href^="#"]', 'click', function(this: HTMLAnchorElement) { return onJumpToLinkClicked(this, ...arguments); });
}
protected executeCommand(command: string | undefined) {
if (command === undefined) return;
private evaluateStateExpression(expression: string, changes: { [key: string]: string | boolean }): boolean {
let state = false;
for (const expr of expression.trim().split('&')) {
const [lhs, op, rhs] = parseStateExpression(expr);
this.log(`${this._appName}.executeCommand: command=${command}`);
switch (op) {
case '=': { // Equals
let value = changes[lhs];
if (value === undefined) {
value = this.getSettingValue<string | boolean>(lhs) || false;
}
state = rhs !== undefined ? rhs === '' + value : !!value;
break;
}
case '!': { // Not equals
let value = changes[lhs];
if (value === undefined) {
value = this.getSettingValue<string | boolean>(lhs) || false;
}
state = rhs !== undefined ? rhs !== '' + 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;
}
this._commandRelay.setAttribute('href', command);
this._commandRelay.click();
private getSettingValue<T>(path: string): T | undefined {
return get<T>(this.bootstrap.config, path);
}
private getScope(): 'user' | 'workspace' {
return this._scopes != null
? this._scopes.options[this._scopes.selectedIndex].value as 'user' | 'workspace'
: 'user';
private initialize() {
this.log(`${this.appName}.initialize`);
this.onInitialize();
this.setState();
}
private initializeColorPalette() {
const onColorThemeChanged = () => {
const body = document.body;
const computedStyle = getComputedStyle(body);
const bodyStyle = body.style;
let color = computedStyle.getPropertyValue('--color').trim();
bodyStyle.setProperty('--color--75', opacity(color, 75));
bodyStyle.setProperty('--color--50', opacity(color, 50));
color = computedStyle.getPropertyValue('--background-color').trim();
bodyStyle.setProperty('--background-color--lighten-05', lighten(color, 5));
bodyStyle.setProperty('--background-color--darken-05', darken(color, 5));
bodyStyle.setProperty('--background-color--lighten-075', lighten(color, 7.5));
bodyStyle.setProperty('--background-color--darken-075', darken(color, 7.5));
bodyStyle.setProperty('--background-color--lighten-15', lighten(color, 15));
bodyStyle.setProperty('--background-color--darken-15', darken(color, 15));
bodyStyle.setProperty('--background-color--lighten-30', lighten(color, 30));
bodyStyle.setProperty('--background-color--darken-30', darken(color, 30));
color = computedStyle.getPropertyValue('--link-color').trim();
bodyStyle.setProperty('--link-color--darken-20', darken(color, 20));
};
const observer = new MutationObserver(onColorThemeChanged);
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
onColorThemeChanged();
return observer;
}
private setState() {
this._updating = true;
try {
for (const el of document.querySelectorAll<HTMLInputElement>('input[type=checkbox].setting')) {
const checked = el.dataset.type === 'array'
? (this.getSettingValue<string[]>(el.name) || []).includes(el.value)
: this.getSettingValue<boolean>(el.name) || false;
el.checked = checked;
}
for (const el of document.querySelectorAll<HTMLInputElement>('input[type=text].setting, input:not([type]).setting')) {
el.value = this.getSettingValue<string>(el.name) || '';
}
for (const el of document.querySelectorAll<HTMLSelectElement>('select.setting')) {
const value = this.getSettingValue<string>(el.name);
const option = el.querySelector<HTMLOptionElement>(`option[value='${value}']`);
if (option != null) {
option.selected = true;
}
}
}
finally {
this._updating = false;
}
const state = flatten(this.bootstrap.config);
this.setVisibility(state);
this.setEnablement(state);
}
private setAdditionalSettings(expression: string | undefined) {
@ -229,29 +370,29 @@ export abstract class App {
private setEnablement(state: { [key: string]: string | boolean }) {
for (const el of document.querySelectorAll<HTMLElement>('[data-enablement]')) {
// Since everything starts disabled, kick out if it still is
if (!evaluateStateExpression(el.dataset.enablement!, state)) continue;
el.removeAttribute('disabled');
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 = false;
(el as HTMLInputElement | HTMLSelectElement).disabled = disabled;
}
else {
const input = el.querySelector<HTMLInputElement | HTMLSelectElement>('input,select');
if (input == null) continue;
input.disabled = false;
input.disabled = disabled;
}
}
}
private setVisibility(state: { [key: string]: string | boolean }) {
for (const el of document.querySelectorAll<HTMLElement>('[data-visibility]')) {
// Since everything starts hidden, kick out if it still is
if (!evaluateStateExpression(el.dataset.visibility!, state)) continue;
el.classList.remove('hidden');
el.classList.toggle('hidden', !this.evaluateStateExpression(el.dataset.visibility!, state));
}
}
}
@ -262,48 +403,34 @@ function ensureIfBoolean(value: string | boolean): string | boolean {
return value;
}
function evaluateStateExpression(expression: string, changes: { [key: string]: string | boolean }): boolean {
let state = false;
for (const expr of expression.trim().split('&')) {
const [lhs, op, rhs] = parseStateExpression(expr);
function get<T>(o: { [key: string ]: any}, path: string): T | undefined {
return path.split('.').reduce((o = {}, key) => o == null ? undefined : o[key], o) as T;
}
switch (op) {
case '=': { // Equals
let value = changes[lhs];
if (value === undefined) {
value = getSettingValue<string | boolean>(lhs) || false;
}
state = rhs !== undefined ? rhs === '' + value : !!value;
break;
}
case '!': { // Not equals
let value = changes[lhs];
if (value === undefined) {
value = getSettingValue<string | boolean>(lhs) || false;
}
state = rhs !== undefined ? rhs !== '' + value : !value;
break;
}
case '+': { // Contains
if (rhs !== undefined) {
const setting = getSettingValue<string[]>(lhs);
state = setting !== undefined ? setting.includes(rhs.toString()) : false;
}
break;
}
function set(o: { [key: string ]: any}, path: string, value: any): { [key: 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
: {};
}
if (!state) break;
nested[key] = newValue;
nested = nested[key];
}
return state;
}
function get<T>(o: { [key: string ]: any}, path: string): T | undefined {
return path.split('.').reduce((o = {}, key) => o[key], o) as T;
}
function getSettingValue<T>(path: string): T | undefined {
return get<T>(gitlens.config, path);
return o;
}
function parseAdditionalSettingsExpression(expression: string): [string, string | boolean][] {
@ -335,4 +462,13 @@ function flatten(o: { [key: string]: any }, path?: string): { [key: string]: any
}
return results;
}
function fromCheckboxValue(elementValue: any) {
switch (elementValue) {
case 'on': return true;
case 'null': return null;
case 'undefined': return undefined;
default: return elementValue;
}
}

+ 24
- 43
src/ui/shared/colors.ts 查看文件

@ -6,8 +6,7 @@ function adjustLight(color: number, amount: number) {
? cc < 0 ? 0 : cc
: cc > 255 ? 255 : cc;
const hex = Math.round(c).toString(16);
return hex.length > 1 ? hex : `0${hex}`;
return Math.round(c);
}
export function darken(color: string, percentage: number) {
@ -15,50 +14,23 @@ export function darken(color: string, percentage: number) {
}
export function lighten(color: string, percentage: number) {
const rgb = toRgb(color);
if (rgb == null) return color;
const rgba = toRgba(color);
if (rgba == null) return color;
const [r, g, b] = rgb;
const [r, g, b, a] = rgba;
percentage = (255 * percentage) / 100;
return `#${adjustLight(r, percentage)}${adjustLight(g, percentage)}${adjustLight(b, percentage)}`;
return `rgba(${adjustLight(r, percentage)}, ${adjustLight(g, percentage)}, ${adjustLight(b, percentage)}, ${a})`
}
export function initializeColorPalette() {
const onColorThemeChanged = () => {
const body = document.body;
const computedStyle = getComputedStyle(body);
export function opacity(color: string, percentage: number) {
const rgba = toRgba(color);
if (rgba == null) return color;
const bodyStyle = body.style;
let color = computedStyle.getPropertyValue('--color').trim();
const rgb = toRgb(color);
if (rgb != null) {
const [r, g, b] = rgb;
bodyStyle.setProperty('--color--75', `rgba(${r}, ${g}, ${b}, 0.75)`);
bodyStyle.setProperty('--color--50', `rgba(${r}, ${g}, ${b}, 0.5)`);
}
color = computedStyle.getPropertyValue('--background-color').trim();
bodyStyle.setProperty('--background-color--lighten-05', lighten(color, 5));
bodyStyle.setProperty('--background-color--darken-05', darken(color, 5));
bodyStyle.setProperty('--background-color--lighten-075', lighten(color, 7.5));
bodyStyle.setProperty('--background-color--darken-075', darken(color, 7.5));
bodyStyle.setProperty('--background-color--lighten-15', lighten(color, 15));
bodyStyle.setProperty('--background-color--darken-15', darken(color, 15));
bodyStyle.setProperty('--background-color--lighten-30', lighten(color, 30));
bodyStyle.setProperty('--background-color--darken-30', darken(color, 30));
color = computedStyle.getPropertyValue('--link-color').trim();
bodyStyle.setProperty('--link-color--darken-20', darken(color, 20));
};
const observer = new MutationObserver(onColorThemeChanged);
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
onColorThemeChanged();
return observer;
const [r, g, b, a] = rgba;
return `rgba(${r}, ${g}, ${b}, ${a * (percentage / 100)})`;
}
export function toRgb(color: string) {
export function toRgba(color: string) {
color = color.trim();
const result = cssColorRegEx.exec(color);
@ -71,13 +43,15 @@ export function toRgb(color: string) {
return [
parseInt(hex[0] + hex[0], 16),
parseInt(hex[1] + hex[1], 16),
parseInt(hex[2] + hex[2], 16)
parseInt(hex[2] + hex[2], 16),
1
];
case 6:
return [
parseInt(hex.substring(0, 2), 16),
parseInt(hex.substring(2, 4), 16),
parseInt(hex.substring(4, 6), 16)
parseInt(hex.substring(4, 6), 16),
1
];
}
@ -86,13 +60,20 @@ export function toRgb(color: string) {
switch (result[3]) {
case 'rgb':
return [
parseInt(result[4], 10),
parseInt(result[5], 10),
parseInt(result[6], 10),
1
];
case 'rgba':
return [
parseInt(result[4], 10),
parseInt(result[5], 10),
parseInt(result[6], 10)
parseInt(result[6], 10),
parseFloat(result[7])
];
default:
return null;
}
}
}

+ 21
- 4
src/ui/welcome/app.ts 查看文件

@ -1,16 +1,23 @@
'use strict';
import { DOM } from './../shared/dom';
import { App } from '../shared/app-base';
import { WelcomeBootstrap } from '../ipc';
export class WelcomeApp extends App {
const bootstrap: WelcomeBootstrap = (window as any).bootstrap;
export class WelcomeApp extends App<WelcomeBootstrap> {
private _commandRelay: HTMLAnchorElement | null | undefined;
constructor() {
super('WelcomeApp');
super('WelcomeApp', bootstrap);
}
protected bind() {
super.bind();
protected onInitialize() {
this._commandRelay = DOM.getElementById<HTMLAnchorElement>('commandRelay');
}
protected onBind() {
const onClicked = this.onClicked.bind(this);
DOM.listenAll('button[data-href]', 'click', function(this: HTMLButtonElement) { onClicked(this); });
}
@ -18,4 +25,14 @@ export class WelcomeApp extends App {
private onClicked(element: HTMLButtonElement) {
this.executeCommand(element.dataset.href);
}
private executeCommand(command: string | undefined) {
if (command === undefined || this._commandRelay == null) return;
this.log(`${this.appName}.executeCommand: command=${command}`);
this._commandRelay.setAttribute('href', command);
this._commandRelay.click();
}
}

+ 1
- 1
src/ui/welcome/index.html 查看文件

@ -342,7 +342,7 @@
</div>
<a id="commandRelay" style="display: none"></a>
<script type="text/javascript">
window.gitlens = '{{data}}';
window.bootstrap = '{{bootstrap}}';
</script>
</body>

+ 46
- 0
src/webviews/settingsEditor.ts 查看文件

@ -0,0 +1,46 @@
'use strict';
import { commands, workspace } from 'vscode';
import { Container } from '../container';
import { SettingsBootstrap } from '../ui/ipc';
import { WebviewEditor } from './webviewEditor';
export class SettingsEditor extends WebviewEditor<SettingsBootstrap> {
constructor() {
super();
}
get filename(): string {
return 'settings.html';
}
get id(): string {
return 'gitlens.settings';
}
get title(): string {
return 'GitLens Settings';
}
getBootstrap() {
return {
config: Container.config,
scope: 'user',
scopes: this.getAvailableScopes()
} as SettingsBootstrap;
}
registerCommands() {
return [
commands.registerCommand('gitlens.showSettingsPage', this.show, this)
];
}
private getAvailableScopes(): ['user' | 'workspace', string][] {
const scopes: ['user' | 'workspace', string][] = [['user', 'User Settings']];
if (workspace.workspaceFolders !== undefined && workspace.workspaceFolders.length) {
scopes.push(['workspace', 'Workspace Settings']);
}
return scopes;
}
}

+ 167
- 0
src/webviews/webviewEditor.ts 查看文件

@ -0,0 +1,167 @@
'use strict';
import { ConfigurationChangeEvent, ConfigurationTarget, Disposable, Uri, ViewColumn, WebviewPanel, WebviewPanelOnDidChangeViewStateEvent, window, workspace } from 'vscode';
import { configuration } from '../configuration';
import { Container } from '../container';
import { Message, SettingsChangedMessage } from '../ui/ipc';
import { Logger } from '../logger';
import * as fs from 'fs';
export abstract class WebviewEditor<TBootstrap> extends Disposable {
private _disposable: Disposable | undefined;
private _disposablePanel: Disposable | undefined;
private _panel: WebviewPanel | undefined;
constructor() {
super(() => this.dispose());
this._disposable = Disposable.from(
configuration.onDidChange(this.onConfigurationChanged, this),
...this.registerCommands()
);
}
abstract get filename(): string;
abstract get id(): string;
abstract get title(): string;
abstract getBootstrap(): TBootstrap;
abstract registerCommands(): Disposable[];
dispose() {
this._disposable && this._disposable.dispose();
this._disposablePanel && this._disposablePanel.dispose();
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
this.postUpdatedConfiguration();
}
private onPanelDisposed() {
this._disposablePanel && this._disposablePanel.dispose();
this._panel = undefined;
}
private _invalidateOnVisible: 'all' | 'config' | undefined;
private onViewStateChanged(e: WebviewPanelOnDidChangeViewStateEvent) {
Logger.log('WebviewEditor.onViewStateChanged', e.webviewPanel.visible);
// HACK: Because messages aren't sent to the webview when hidden, we need make sure it is up-to-date
if (this._invalidateOnVisible && e.webviewPanel.visible) {
const invalidates = this._invalidateOnVisible;
this._invalidateOnVisible = undefined;
switch (invalidates) {
case 'config':
this.postUpdatedConfiguration();
break;
default:
this.show();
break;
}
}
}
protected async onMessageReceived(e: Message) {
if (e == null) return;
Logger.log(`WebviewEditor.onMessageReceived: type=${e.type}, data=${JSON.stringify(e)}`);
switch (e.type) {
case 'saveSettings':
const target = e.scope === 'workspace'
? ConfigurationTarget.Workspace
: ConfigurationTarget.Global;
for (const key in e.changes) {
const inspect = await configuration.inspect(key)!;
const value = e.changes[key];
await configuration.update(key, value === inspect.defaultValue ? undefined : value, target);
}
for (const key of e.removes) {
await configuration.update(key, undefined, target);
}
break;
}
}
get visible() {
return this._panel === undefined ? false : this._panel.visible;
}
hide() {
if (this._panel === undefined) return;
this._panel.dispose();
}
async show(): Promise<void> {
let html = (await this.getHtml())
.replace(/{{root}}/g, Uri.file(Container.context.asAbsolutePath('.')).with({ scheme: 'vscode-resource' }).toString());
if (html.includes('\'{{bootstrap}}\'')) {
html = html.replace(/'{{bootstrap}}'/g, JSON.stringify(this.getBootstrap()));
}
if (this._panel === undefined) {
this._panel = window.createWebviewPanel(
this.id,
this.title,
ViewColumn.Active, // { viewColumn: ViewColumn.Active, preserveFocus: false }
{
retainContextWhenHidden: true,
enableFindWidget: true,
enableCommandUris: true,
enableScripts: true
}
);
this._disposablePanel = Disposable.from(
this._panel,
this._panel.onDidDispose(this.onPanelDisposed, this),
this._panel.onDidChangeViewState(this.onViewStateChanged, this),
this._panel.webview.onDidReceiveMessage(this.onMessageReceived, this)
);
this._panel.webview.html = html;
}
else {
this._panel.webview.html = html;
this._panel.reveal(ViewColumn.Active); // , false);
}
}
private async getHtml(): Promise<string> {
if (Logger.isDebugging) {
return new Promise<string>((resolve, reject) => {
fs.readFile(Container.context.asAbsolutePath(this.filename), 'utf8', (err, data) => {
if (err) {
reject(err);
}
else {
resolve(data);
}
});
});
}
const doc = await workspace.openTextDocument(Container.context.asAbsolutePath(this.filename));
return doc.getText();
}
private postMessage(message: Message, invalidates: 'all' | 'config' = 'all') {
if (this._panel === undefined) return false;
const result = this._panel!.webview.postMessage(message);
if (!result && this._invalidateOnVisible !== 'all') {
this._invalidateOnVisible = invalidates;
}
return result;
}
private postUpdatedConfiguration() {
return this.postMessage({ type: 'settingsChanged', config: Container.config } as SettingsChangedMessage, 'config');
}
}

+ 35
- 0
src/webviews/welcomeEditor.ts 查看文件

@ -0,0 +1,35 @@
'use strict';
import { commands } from 'vscode';
import { Container } from '../container';
import { WelcomeBootstrap } from '../ui/ipc';
import { WebviewEditor } from './webviewEditor';
export class WelcomeEditor extends WebviewEditor<WelcomeBootstrap> {
constructor() {
super();
}
get filename(): string {
return 'welcome.html';
}
get id(): string {
return 'gitlens.welcome';
}
get title(): string {
return 'Welcome to GitLens';
}
getBootstrap(): WelcomeBootstrap {
return {
config: Container.config
};
}
registerCommands() {
return [
commands.registerCommand('gitlens.showWelcomePage', this.show, this)
];
}
}

Loading…
取消
儲存