mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-09 09:42:34 -05:00
Merge branch 'ads-main-vscode-2020-07-15T23-51-12' into main
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withBrowserDefaults = require('../shared.webpack.config').browser;
|
||||
|
||||
module.exports = withBrowserDefaults({
|
||||
context: __dirname,
|
||||
entry: {
|
||||
extension: './src/extensionEditingBrowserMain.ts'
|
||||
},
|
||||
output: {
|
||||
filename: 'extensionEditingBrowserMain.js'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ const withDefaults = require('../shared.webpack.config');
|
||||
module.exports = withDefaults({
|
||||
context: __dirname,
|
||||
entry: {
|
||||
extension: './src/extension.ts',
|
||||
extension: './src/extensionEditingMain.ts',
|
||||
},
|
||||
externals: {
|
||||
'../../../product.json': 'commonjs ../../../product.json',
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"onLanguage:markdown",
|
||||
"onLanguage:typescript"
|
||||
],
|
||||
"main": "./out/extension",
|
||||
"main": "./out/extensionEditingMain",
|
||||
"browser": "./dist/browser/extensionEditingBrowserMain",
|
||||
"scripts": {
|
||||
"compile": "gulp compile-extension:extension-editing",
|
||||
"watch": "gulp watch-extension:extension-editing"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { PackageDocument } from './packageDocumentHelper';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
//package.json suggestions
|
||||
context.subscriptions.push(registerPackageDocumentCompletions());
|
||||
|
||||
}
|
||||
|
||||
function registerPackageDocumentCompletions(): vscode.Disposable {
|
||||
return vscode.languages.registerCompletionItemProvider({ language: 'json', pattern: '**/package.json' }, {
|
||||
provideCompletionItems(document, position, token) {
|
||||
return new PackageDocument(document).provideCompletionItems(position, token);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1789,8 +1789,8 @@
|
||||
"diff"
|
||||
],
|
||||
"extensions": [
|
||||
".patch",
|
||||
".diff",
|
||||
".patch",
|
||||
".rej"
|
||||
],
|
||||
"configuration": "./languages/diff.language-configuration.json"
|
||||
@@ -1878,8 +1878,8 @@
|
||||
"dependencies": {
|
||||
"byline": "^5.0.0",
|
||||
"file-type": "^7.2.0",
|
||||
"iconv-lite-umd": "0.6.5",
|
||||
"jschardet": "2.1.1",
|
||||
"iconv-lite-umd": "0.6.8",
|
||||
"jschardet": "2.2.1",
|
||||
"vscode-extension-telemetry": "0.1.1",
|
||||
"vscode-nls": "^4.0.0",
|
||||
"vscode-uri": "^2.0.0",
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
|
||||
import { Model } from '../model';
|
||||
import { Repository as BaseRepository, Resource } from '../repository';
|
||||
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, GitExtension, RefType, RemoteSourceProvider, CredentialsProvider, BranchQuery } from './git';
|
||||
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, RemoteSourceProvider, CredentialsProvider, BranchQuery, PushErrorHandler } from './git';
|
||||
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands } from 'vscode';
|
||||
import { mapEvent } from '../util';
|
||||
import { toGitUri } from '../uri';
|
||||
import { pickRemoteSource, PickRemoteSourceOptions } from '../remoteSource';
|
||||
import { GitExtensionImpl } from './extension';
|
||||
|
||||
class ApiInputBox implements InputBox {
|
||||
set value(value: string) { this._inputBox.value = value; }
|
||||
@@ -271,6 +273,10 @@ export class ApiImpl implements API {
|
||||
return this._model.registerCredentialsProvider(provider);
|
||||
}
|
||||
|
||||
registerPushErrorHandler(handler: PushErrorHandler): Disposable {
|
||||
return this._model.registerPushErrorHandler(handler);
|
||||
}
|
||||
|
||||
constructor(private _model: Model) { }
|
||||
}
|
||||
|
||||
@@ -308,41 +314,51 @@ function getStatus(status: Status): string {
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
|
||||
export function registerAPICommands(extension: GitExtension): Disposable {
|
||||
return Disposable.from(
|
||||
commands.registerCommand('git.api.getRepositories', () => {
|
||||
const api = extension.getAPI(1);
|
||||
return api.repositories.map(r => r.rootUri.toString());
|
||||
}),
|
||||
export function registerAPICommands(extension: GitExtensionImpl): Disposable {
|
||||
const disposables: Disposable[] = [];
|
||||
|
||||
commands.registerCommand('git.api.getRepositoryState', (uri: string) => {
|
||||
const api = extension.getAPI(1);
|
||||
const repository = api.getRepository(Uri.parse(uri));
|
||||
disposables.push(commands.registerCommand('git.api.getRepositories', () => {
|
||||
const api = extension.getAPI(1);
|
||||
return api.repositories.map(r => r.rootUri.toString());
|
||||
}));
|
||||
|
||||
if (!repository) {
|
||||
return null;
|
||||
}
|
||||
disposables.push(commands.registerCommand('git.api.getRepositoryState', (uri: string) => {
|
||||
const api = extension.getAPI(1);
|
||||
const repository = api.getRepository(Uri.parse(uri));
|
||||
|
||||
const state = repository.state;
|
||||
if (!repository) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ref = (ref: Ref | undefined) => (ref && { ...ref, type: getRefType(ref.type) });
|
||||
const change = (change: Change) => ({
|
||||
uri: change.uri.toString(),
|
||||
originalUri: change.originalUri.toString(),
|
||||
renameUri: change.renameUri?.toString(),
|
||||
status: getStatus(change.status)
|
||||
});
|
||||
const state = repository.state;
|
||||
|
||||
return {
|
||||
HEAD: ref(state.HEAD),
|
||||
refs: state.refs.map(ref),
|
||||
remotes: state.remotes,
|
||||
submodules: state.submodules,
|
||||
rebaseCommit: state.rebaseCommit,
|
||||
mergeChanges: state.mergeChanges.map(change),
|
||||
indexChanges: state.indexChanges.map(change),
|
||||
workingTreeChanges: state.workingTreeChanges.map(change)
|
||||
};
|
||||
})
|
||||
);
|
||||
const ref = (ref: Ref | undefined) => (ref && { ...ref, type: getRefType(ref.type) });
|
||||
const change = (change: Change) => ({
|
||||
uri: change.uri.toString(),
|
||||
originalUri: change.originalUri.toString(),
|
||||
renameUri: change.renameUri?.toString(),
|
||||
status: getStatus(change.status)
|
||||
});
|
||||
|
||||
return {
|
||||
HEAD: ref(state.HEAD),
|
||||
refs: state.refs.map(ref),
|
||||
remotes: state.remotes,
|
||||
submodules: state.submodules,
|
||||
rebaseCommit: state.rebaseCommit,
|
||||
mergeChanges: state.mergeChanges.map(change),
|
||||
indexChanges: state.indexChanges.map(change),
|
||||
workingTreeChanges: state.workingTreeChanges.map(change)
|
||||
};
|
||||
}));
|
||||
|
||||
disposables.push(commands.registerCommand('git.api.getRemoteSources', (opts?: PickRemoteSourceOptions) => {
|
||||
if (!extension.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
return pickRemoteSource(extension.model, opts);
|
||||
}));
|
||||
|
||||
return Disposable.from(...disposables);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Model } from '../model';
|
||||
import { GitExtension, Repository, API } from './git';
|
||||
import { ApiRepository, ApiImpl } from './api1';
|
||||
import { Event, EventEmitter } from 'vscode';
|
||||
import { latchEvent } from '../util';
|
||||
|
||||
export function deprecated(_target: any, key: string, descriptor: any): void {
|
||||
if (typeof descriptor.value !== 'function') {
|
||||
@@ -26,17 +25,27 @@ export class GitExtensionImpl implements GitExtension {
|
||||
enabled: boolean = false;
|
||||
|
||||
private _onDidChangeEnablement = new EventEmitter<boolean>();
|
||||
readonly onDidChangeEnablement: Event<boolean> = latchEvent(this._onDidChangeEnablement.event);
|
||||
readonly onDidChangeEnablement: Event<boolean> = this._onDidChangeEnablement.event;
|
||||
|
||||
private _model: Model | undefined = undefined;
|
||||
|
||||
set model(model: Model | undefined) {
|
||||
this._model = model;
|
||||
|
||||
this.enabled = !!model;
|
||||
const enabled = !!model;
|
||||
|
||||
if (this.enabled === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enabled = enabled;
|
||||
this._onDidChangeEnablement.fire(this.enabled);
|
||||
}
|
||||
|
||||
get model(): Model | undefined {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
constructor(model?: Model) {
|
||||
if (model) {
|
||||
this.enabled = true;
|
||||
@@ -73,4 +82,4 @@ export class GitExtensionImpl implements GitExtension {
|
||||
|
||||
return new ApiImpl(this._model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
extensions/git/src/api/git.d.ts
vendored
6
extensions/git/src/api/git.d.ts
vendored
@@ -223,6 +223,10 @@ export interface CredentialsProvider {
|
||||
getCredentials(host: Uri): ProviderResult<Credentials>;
|
||||
}
|
||||
|
||||
export interface PushErrorHandler {
|
||||
handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean>;
|
||||
}
|
||||
|
||||
export type APIState = 'uninitialized' | 'initialized';
|
||||
|
||||
export interface API {
|
||||
@@ -239,6 +243,7 @@ export interface API {
|
||||
|
||||
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
|
||||
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
|
||||
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
|
||||
}
|
||||
|
||||
export interface GitExtension {
|
||||
@@ -276,6 +281,7 @@ export const enum GitErrorCodes {
|
||||
CantOpenResource = 'CantOpenResource',
|
||||
GitNotFound = 'GitNotFound',
|
||||
CantCreatePipe = 'CantCreatePipe',
|
||||
PermissionDenied = 'PermissionDenied',
|
||||
CantAccessRemote = 'CantAccessRemote',
|
||||
RepositoryNotFound = 'RepositoryNotFound',
|
||||
RepositoryIsLocked = 'RepositoryIsLocked',
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
import { lstat, Stats } from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, QuickPick } from 'vscode';
|
||||
import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env } from 'vscode';
|
||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Branch, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourceProvider, RemoteSource } from './api/git';
|
||||
import { Branch, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourceProvider } from './api/git';
|
||||
import { ForcePushMode, Git, Stash } from './git';
|
||||
import { Model } from './model';
|
||||
import { Repository, Resource, ResourceGroupType } from './repository';
|
||||
@@ -18,8 +18,8 @@ import { fromGitUri, toGitUri, isGitUri } from './uri';
|
||||
import { grep, isDescendant, pathEquals } from './util';
|
||||
import { Log, LogLevel } from './log';
|
||||
import { GitTimelineItem } from './timelineProvider';
|
||||
import { throttle, debounce } from './decorators';
|
||||
import { ApiRepository } from './api/api1';
|
||||
import { pickRemoteSource } from './remoteSource';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -240,72 +240,6 @@ interface PushOptions {
|
||||
silent?: boolean;
|
||||
}
|
||||
|
||||
async function getQuickPickResult<T extends QuickPickItem>(quickpick: QuickPick<T>): Promise<T | undefined> {
|
||||
const result = await new Promise<T | undefined>(c => {
|
||||
quickpick.onDidAccept(() => c(quickpick.selectedItems[0]));
|
||||
quickpick.onDidHide(() => c(undefined));
|
||||
quickpick.show();
|
||||
});
|
||||
|
||||
quickpick.hide();
|
||||
return result;
|
||||
}
|
||||
|
||||
class RemoteSourceProviderQuickPick {
|
||||
|
||||
private quickpick: QuickPick<QuickPickItem & { remoteSource?: RemoteSource }>;
|
||||
|
||||
constructor(private provider: RemoteSourceProvider) {
|
||||
this.quickpick = window.createQuickPick();
|
||||
this.quickpick.ignoreFocusOut = true;
|
||||
|
||||
if (provider.supportsQuery) {
|
||||
this.quickpick.placeholder = localize('type to search', "Repository name (type to search)");
|
||||
this.quickpick.onDidChangeValue(this.onDidChangeValue, this);
|
||||
} else {
|
||||
this.quickpick.placeholder = localize('type to filter', "Repository name");
|
||||
}
|
||||
}
|
||||
|
||||
@debounce(300)
|
||||
onDidChangeValue(): void {
|
||||
this.query();
|
||||
}
|
||||
|
||||
@throttle
|
||||
async query(): Promise<void> {
|
||||
this.quickpick.busy = true;
|
||||
|
||||
try {
|
||||
const remoteSources = await this.provider.getRemoteSources(this.quickpick.value) || [];
|
||||
|
||||
if (remoteSources.length === 0) {
|
||||
this.quickpick.items = [{
|
||||
label: localize('none found', "No remote repositories found."),
|
||||
alwaysShow: true
|
||||
}];
|
||||
} else {
|
||||
this.quickpick.items = remoteSources.map(remoteSource => ({
|
||||
label: remoteSource.name,
|
||||
description: remoteSource.description || (typeof remoteSource.url === 'string' ? remoteSource.url : remoteSource.url[0]),
|
||||
remoteSource
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
this.quickpick.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }];
|
||||
console.error(err);
|
||||
} finally {
|
||||
this.quickpick.busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async pick(): Promise<RemoteSource | undefined> {
|
||||
this.query();
|
||||
const result = await getQuickPickResult(this.quickpick);
|
||||
return result?.remoteSource;
|
||||
}
|
||||
}
|
||||
|
||||
export class CommandCenter {
|
||||
|
||||
private disposables: Disposable[];
|
||||
@@ -527,51 +461,10 @@ export class CommandCenter {
|
||||
@command('git.clone')
|
||||
async clone(url?: string, parentPath?: string): Promise<void> {
|
||||
if (!url) {
|
||||
const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider, url?: string })>();
|
||||
quickpick.ignoreFocusOut = true;
|
||||
|
||||
const providers = this.model.getRemoteProviders()
|
||||
.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('clonefrom', "Clone from {0}", provider.name), alwaysShow: true, provider }));
|
||||
|
||||
quickpick.placeholder = providers.length === 0
|
||||
? localize('provide url', "Provide repository URL")
|
||||
: localize('provide url or pick', "Provide repository URL or pick a repository source.");
|
||||
|
||||
const updatePicks = (value?: string) => {
|
||||
if (value) {
|
||||
quickpick.items = [{
|
||||
label: localize('repourl', "Clone from URL"),
|
||||
description: value,
|
||||
alwaysShow: true,
|
||||
url: value
|
||||
},
|
||||
...providers];
|
||||
} else {
|
||||
quickpick.items = providers;
|
||||
}
|
||||
};
|
||||
|
||||
quickpick.onDidChangeValue(updatePicks);
|
||||
updatePicks();
|
||||
|
||||
const result = await getQuickPickResult(quickpick);
|
||||
|
||||
if (result) {
|
||||
if (result.url) {
|
||||
url = result.url;
|
||||
} else if (result.provider) {
|
||||
const quickpick = new RemoteSourceProviderQuickPick(result.provider);
|
||||
const remote = await quickpick.pick();
|
||||
|
||||
if (remote) {
|
||||
if (typeof remote.url === 'string') {
|
||||
url = remote.url;
|
||||
} else if (remote.url.length > 0) {
|
||||
url = await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: localize('pick url', "Choose a URL to clone from.") });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
url = await pickRemoteSource(this.model, {
|
||||
providerLabel: provider => localize('clonefrom', "Clone from {0}", provider.name),
|
||||
urlLabel: localize('repourl', "Clone from URL")
|
||||
});
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
@@ -845,7 +738,7 @@ export class CommandCenter {
|
||||
try {
|
||||
document = await workspace.openTextDocument(uri);
|
||||
} catch (error) {
|
||||
await commands.executeCommand<void>('vscode.open', uri, opts);
|
||||
await commands.executeCommand('vscode.open', uri, opts);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -858,7 +751,7 @@ export class CommandCenter {
|
||||
const editor = await window.showTextDocument(document, opts);
|
||||
editor.revealRange(previousVisibleRanges[0]);
|
||||
} else {
|
||||
await window.showTextDocument(document, opts);
|
||||
await commands.executeCommand('vscode.open', uri, opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2146,52 +2039,10 @@ export class CommandCenter {
|
||||
|
||||
@command('git.addRemote', { repository: true })
|
||||
async addRemote(repository: Repository): Promise<string | undefined> {
|
||||
const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider, url?: string })>();
|
||||
quickpick.ignoreFocusOut = true;
|
||||
|
||||
const providers = this.model.getRemoteProviders()
|
||||
.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('addfrom', "Add remote from {0}", provider.name), alwaysShow: true, provider }));
|
||||
|
||||
quickpick.placeholder = providers.length === 0
|
||||
? localize('provide url', "Provide repository URL")
|
||||
: localize('provide url or pick', "Provide repository URL or pick a repository source.");
|
||||
|
||||
const updatePicks = (value?: string) => {
|
||||
if (value) {
|
||||
quickpick.items = [{
|
||||
label: localize('addFrom', "Add remote from URL"),
|
||||
description: value,
|
||||
alwaysShow: true,
|
||||
url: value
|
||||
},
|
||||
...providers];
|
||||
} else {
|
||||
quickpick.items = providers;
|
||||
}
|
||||
};
|
||||
|
||||
quickpick.onDidChangeValue(updatePicks);
|
||||
updatePicks();
|
||||
|
||||
const result = await getQuickPickResult(quickpick);
|
||||
let url: string | undefined;
|
||||
|
||||
if (result) {
|
||||
if (result.url) {
|
||||
url = result.url;
|
||||
} else if (result.provider) {
|
||||
const quickpick = new RemoteSourceProviderQuickPick(result.provider);
|
||||
const remote = await quickpick.pick();
|
||||
|
||||
if (remote) {
|
||||
if (typeof remote.url === 'string') {
|
||||
url = remote.url;
|
||||
} else if (remote.url.length > 0) {
|
||||
url = await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: localize('pick url', "Choose a URL to clone from.") });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const url = await pickRemoteSource(this.model, {
|
||||
providerLabel: provider => localize('addfrom', "Add remote from {0}", provider.name),
|
||||
urlLabel: localize('addFrom', "Add remote from URL")
|
||||
});
|
||||
|
||||
if (!url) {
|
||||
return;
|
||||
@@ -2533,8 +2384,7 @@ export class CommandCenter {
|
||||
|
||||
@command('git.timeline.openDiff', { repository: false })
|
||||
async timelineOpenDiff(item: TimelineItem, uri: Uri | undefined, _source: string) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (uri == null || !GitTimelineItem.is(item)) {
|
||||
if (uri === undefined || uri === null || !GitTimelineItem.is(item)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -419,7 +419,7 @@ export class Git {
|
||||
}
|
||||
|
||||
async getRepositoryRoot(repositoryPath: string): Promise<string> {
|
||||
const result = await this.exec(repositoryPath, ['rev-parse', '--show-toplevel']);
|
||||
const result = await this.exec(repositoryPath, ['rev-parse', '--show-toplevel'], { log: false });
|
||||
|
||||
// Keep trailing spaces which are part of the directory name
|
||||
const repoPath = path.normalize(result.stdout.trimLeft().replace(/[\r\n]+$/, ''));
|
||||
@@ -437,8 +437,7 @@ export class Git {
|
||||
try {
|
||||
const networkPath = await new Promise<string>(resolve =>
|
||||
realpath.native(`${letter}:`, { encoding: 'utf8' }, (err, resolvedPath) =>
|
||||
// eslint-disable-next-line eqeqeq
|
||||
resolve(err != null ? undefined : resolvedPath),
|
||||
resolve(err !== null ? undefined : resolvedPath),
|
||||
),
|
||||
);
|
||||
if (networkPath !== undefined) {
|
||||
@@ -1628,6 +1627,8 @@ export class Repository {
|
||||
err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
|
||||
} else if (/^fatal: The current branch .* has no upstream branch/.test(err.stderr || '')) {
|
||||
err.gitErrorCode = GitErrorCodes.NoUpstreamBranch;
|
||||
} else if (/Permission.*denied/.test(err.stderr || '')) {
|
||||
err.gitErrorCode = GitErrorCodes.PermissionDenied;
|
||||
}
|
||||
|
||||
throw err;
|
||||
|
||||
@@ -128,7 +128,7 @@ async function warnAboutMissingGit(): Promise<void> {
|
||||
}
|
||||
}*/
|
||||
|
||||
export async function _activate(context: ExtensionContext): Promise<GitExtension> {
|
||||
export async function _activate(context: ExtensionContext): Promise<GitExtensionImpl> {
|
||||
const disposables: Disposable[] = [];
|
||||
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
|
||||
|
||||
@@ -176,8 +176,7 @@ export async function activate(context: ExtensionContext): Promise<GitExtension>
|
||||
return result;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
async function checkGitVersion(info: IGit): Promise<void> {
|
||||
async function checkGitv1(info: IGit): Promise<void> {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const shouldIgnore = config.get<boolean>('ignoreLegacyWarning') === true;
|
||||
|
||||
@@ -204,3 +203,28 @@ async function checkGitVersion(info: IGit): Promise<void> {
|
||||
await config.update('ignoreLegacyWarning', true, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkGitWindows(info: IGit): Promise<void> {
|
||||
if (!/^2\.(25|26)\./.test(info.version)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const update = localize('updateGit', "Update Git");
|
||||
const choice = await window.showWarningMessage(
|
||||
localize('git2526', "There are known issues with the installed Git {0}. Please update to Git >= 2.27 for the git features to work correctly.", info.version),
|
||||
update
|
||||
);
|
||||
|
||||
if (choice === update) {
|
||||
commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/'));
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
async function checkGitVersion(info: IGit): Promise<void> {
|
||||
await checkGitv1(info);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
await checkGitWindows(info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,10 @@ import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { fromGitUri } from './uri';
|
||||
import { APIState as State, RemoteSourceProvider, CredentialsProvider } from './api/git';
|
||||
import { APIState as State, RemoteSourceProvider, CredentialsProvider, PushErrorHandler } from './api/git';
|
||||
import { Askpass } from './askpass';
|
||||
import { IRemoteSourceProviderRegistry } from './remoteProvider';
|
||||
import { IPushErrorHandlerRegistry } from './pushError';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -46,7 +47,7 @@ interface OpenRepository extends Disposable {
|
||||
repository: Repository;
|
||||
}
|
||||
|
||||
export class Model implements IRemoteSourceProviderRegistry {
|
||||
export class Model implements IRemoteSourceProviderRegistry, IPushErrorHandlerRegistry {
|
||||
|
||||
private _onDidOpenRepository = new EventEmitter<Repository>();
|
||||
readonly onDidOpenRepository: Event<Repository> = this._onDidOpenRepository.event;
|
||||
@@ -94,6 +95,8 @@ export class Model implements IRemoteSourceProviderRegistry {
|
||||
private _onDidRemoveRemoteSourceProvider = new EventEmitter<RemoteSourceProvider>();
|
||||
readonly onDidRemoveRemoteSourceProvider = this._onDidRemoveRemoteSourceProvider.event;
|
||||
|
||||
private pushErrorHandlers = new Set<PushErrorHandler>();
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private outputChannel: OutputChannel) {
|
||||
@@ -269,7 +272,7 @@ export class Model implements IRemoteSourceProviderRegistry {
|
||||
}
|
||||
|
||||
const dotGit = await this.git.getRepositoryDotGit(repositoryRoot);
|
||||
const repository = new Repository(this.git.open(repositoryRoot, dotGit), this, this.globalState, this.outputChannel);
|
||||
const repository = new Repository(this.git.open(repositoryRoot, dotGit), this, this, this.globalState, this.outputChannel);
|
||||
|
||||
this.open(repository);
|
||||
await repository.status();
|
||||
@@ -485,6 +488,15 @@ export class Model implements IRemoteSourceProviderRegistry {
|
||||
return [...this.remoteSourceProviders.values()];
|
||||
}
|
||||
|
||||
registerPushErrorHandler(handler: PushErrorHandler): Disposable {
|
||||
this.pushErrorHandlers.add(handler);
|
||||
return toDisposable(() => this.pushErrorHandlers.delete(handler));
|
||||
}
|
||||
|
||||
getPushErrorHandlers(): PushErrorHandler[] {
|
||||
return [...this.pushErrorHandlers];
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
const openRepositories = [...this.openRepositories];
|
||||
openRepositories.forEach(r => r.dispose());
|
||||
|
||||
12
extensions/git/src/pushError.ts
Normal file
12
extensions/git/src/pushError.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vscode';
|
||||
import { PushErrorHandler } from './api/git';
|
||||
|
||||
export interface IPushErrorHandlerRegistry {
|
||||
registerPushErrorHandler(provider: PushErrorHandler): Disposable;
|
||||
getPushErrorHandlers(): PushErrorHandler[];
|
||||
}
|
||||
133
extensions/git/src/remoteSource.ts
Normal file
133
extensions/git/src/remoteSource.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { QuickPickItem, window, QuickPick } from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { RemoteSourceProvider, RemoteSource } from './api/git';
|
||||
import { Model } from './model';
|
||||
import { throttle, debounce } from './decorators';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
async function getQuickPickResult<T extends QuickPickItem>(quickpick: QuickPick<T>): Promise<T | undefined> {
|
||||
const result = await new Promise<T | undefined>(c => {
|
||||
quickpick.onDidAccept(() => c(quickpick.selectedItems[0]));
|
||||
quickpick.onDidHide(() => c(undefined));
|
||||
quickpick.show();
|
||||
});
|
||||
|
||||
quickpick.hide();
|
||||
return result;
|
||||
}
|
||||
|
||||
class RemoteSourceProviderQuickPick {
|
||||
|
||||
private quickpick: QuickPick<QuickPickItem & { remoteSource?: RemoteSource }>;
|
||||
|
||||
constructor(private provider: RemoteSourceProvider) {
|
||||
this.quickpick = window.createQuickPick();
|
||||
this.quickpick.ignoreFocusOut = true;
|
||||
|
||||
if (provider.supportsQuery) {
|
||||
this.quickpick.placeholder = localize('type to search', "Repository name (type to search)");
|
||||
this.quickpick.onDidChangeValue(this.onDidChangeValue, this);
|
||||
} else {
|
||||
this.quickpick.placeholder = localize('type to filter', "Repository name");
|
||||
}
|
||||
}
|
||||
|
||||
@debounce(300)
|
||||
private onDidChangeValue(): void {
|
||||
this.query();
|
||||
}
|
||||
|
||||
@throttle
|
||||
private async query(): Promise<void> {
|
||||
this.quickpick.busy = true;
|
||||
|
||||
try {
|
||||
const remoteSources = await this.provider.getRemoteSources(this.quickpick.value) || [];
|
||||
|
||||
if (remoteSources.length === 0) {
|
||||
this.quickpick.items = [{
|
||||
label: localize('none found', "No remote repositories found."),
|
||||
alwaysShow: true
|
||||
}];
|
||||
} else {
|
||||
this.quickpick.items = remoteSources.map(remoteSource => ({
|
||||
label: remoteSource.name,
|
||||
description: remoteSource.description || (typeof remoteSource.url === 'string' ? remoteSource.url : remoteSource.url[0]),
|
||||
remoteSource
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
this.quickpick.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }];
|
||||
console.error(err);
|
||||
} finally {
|
||||
this.quickpick.busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async pick(): Promise<RemoteSource | undefined> {
|
||||
this.query();
|
||||
const result = await getQuickPickResult(this.quickpick);
|
||||
return result?.remoteSource;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PickRemoteSourceOptions {
|
||||
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
|
||||
readonly urlLabel?: string;
|
||||
}
|
||||
|
||||
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise<string | undefined> {
|
||||
const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider, url?: string })>();
|
||||
quickpick.ignoreFocusOut = true;
|
||||
|
||||
const providers = model.getRemoteProviders()
|
||||
.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + (options.providerLabel ? options.providerLabel(provider) : provider.name), alwaysShow: true, provider }));
|
||||
|
||||
quickpick.placeholder = providers.length === 0
|
||||
? localize('provide url', "Provide repository URL")
|
||||
: localize('provide url or pick', "Provide repository URL or pick a repository source.");
|
||||
|
||||
const updatePicks = (value?: string) => {
|
||||
if (value) {
|
||||
quickpick.items = [{
|
||||
label: options.urlLabel ?? localize('url', "URL"),
|
||||
description: value,
|
||||
alwaysShow: true,
|
||||
url: value
|
||||
},
|
||||
...providers];
|
||||
} else {
|
||||
quickpick.items = providers;
|
||||
}
|
||||
};
|
||||
|
||||
quickpick.onDidChangeValue(updatePicks);
|
||||
updatePicks();
|
||||
|
||||
const result = await getQuickPickResult(quickpick);
|
||||
|
||||
if (result) {
|
||||
if (result.url) {
|
||||
return result.url;
|
||||
} else if (result.provider) {
|
||||
const quickpick = new RemoteSourceProviderQuickPick(result.provider);
|
||||
const remote = await quickpick.pick();
|
||||
|
||||
if (remote) {
|
||||
if (typeof remote.url === 'string') {
|
||||
return remote.url;
|
||||
} else if (remote.url.length > 0) {
|
||||
return await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: localize('pick url', "Choose a URL to clone from.") });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable,
|
||||
import { IFileWatcher, watch } from './watch';
|
||||
import { Log, LogLevel } from './log';
|
||||
import { IRemoteSourceProviderRegistry } from './remoteProvider';
|
||||
import { IPushErrorHandlerRegistry } from './pushError';
|
||||
import { ApiRepository } from './api/api1';
|
||||
|
||||
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
|
||||
|
||||
@@ -683,6 +685,7 @@ export class Repository implements Disposable {
|
||||
constructor(
|
||||
private readonly repository: BaseRepository,
|
||||
remoteSourceProviderRegistry: IRemoteSourceProviderRegistry,
|
||||
private pushErrorHandlerRegistry: IPushErrorHandlerRegistry,
|
||||
globalState: Memento,
|
||||
outputChannel: OutputChannel
|
||||
) {
|
||||
@@ -865,7 +868,7 @@ export class Repository implements Disposable {
|
||||
}
|
||||
|
||||
async getInputTemplate(): Promise<string> {
|
||||
const commitMessage = (await Promise.all([this.repository.getMergeMessage(), this.repository.getSquashMessage()])).find(msg => msg !== undefined);
|
||||
const commitMessage = (await Promise.all([this.repository.getMergeMessage(), this.repository.getSquashMessage()])).find(msg => !!msg);
|
||||
|
||||
if (commitMessage) {
|
||||
return commitMessage;
|
||||
@@ -1181,15 +1184,15 @@ export class Repository implements Disposable {
|
||||
branch = `${head.name}:${head.upstream.name}`;
|
||||
}
|
||||
|
||||
await this.run(Operation.Push, () => this.repository.push(remote, branch, undefined, undefined, forcePushMode));
|
||||
await this.run(Operation.Push, () => this._push(remote, branch, undefined, undefined, forcePushMode));
|
||||
}
|
||||
|
||||
async pushTo(remote?: string, name?: string, setUpstream: boolean = false, forcePushMode?: ForcePushMode): Promise<void> {
|
||||
await this.run(Operation.Push, () => this.repository.push(remote, name, setUpstream, undefined, forcePushMode));
|
||||
await this.run(Operation.Push, () => this._push(remote, name, setUpstream, undefined, forcePushMode));
|
||||
}
|
||||
|
||||
async pushFollowTags(remote?: string, forcePushMode?: ForcePushMode): Promise<void> {
|
||||
await this.run(Operation.Push, () => this.repository.push(remote, undefined, false, true, forcePushMode));
|
||||
await this.run(Operation.Push, () => this._push(remote, undefined, false, true, forcePushMode));
|
||||
}
|
||||
|
||||
async blame(path: string): Promise<string> {
|
||||
@@ -1249,7 +1252,7 @@ export class Repository implements Disposable {
|
||||
const shouldPush = this.HEAD && (typeof this.HEAD.ahead === 'number' ? this.HEAD.ahead > 0 : true);
|
||||
|
||||
if (shouldPush) {
|
||||
await this.repository.push(remoteName, pushBranch);
|
||||
await this._push(remoteName, pushBranch);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1411,6 +1414,31 @@ export class Repository implements Disposable {
|
||||
return ignored;
|
||||
}
|
||||
|
||||
private async _push(remote?: string, refspec?: string, setUpstream: boolean = false, tags = false, forcePushMode?: ForcePushMode): Promise<void> {
|
||||
try {
|
||||
await this.repository.push(remote, refspec, setUpstream, tags, forcePushMode);
|
||||
} catch (err) {
|
||||
if (!remote || !refspec) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const repository = new ApiRepository(this);
|
||||
const remoteObj = repository.state.remotes.find(r => r.name === remote);
|
||||
|
||||
if (!remoteObj) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
for (const handler of this.pushErrorHandlerRegistry.getPushErrorHandlers()) {
|
||||
if (await handler.handlePushError(repository, remoteObj, refspec, err)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async run<T>(operation: Operation, runOperation: () => Promise<T> = () => Promise.resolve<any>(null)): Promise<T> {
|
||||
if (this.state !== RepositoryState.Idle) {
|
||||
throw new Error('Repository not initialized');
|
||||
|
||||
@@ -44,18 +44,6 @@ export function filterEvent<T>(event: Event<T>, filter: (e: T) => boolean): Even
|
||||
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
|
||||
}
|
||||
|
||||
export function latchEvent<T>(event: Event<T>): Event<T> {
|
||||
let firstCall = true;
|
||||
let cache: T;
|
||||
|
||||
return filterEvent(event, value => {
|
||||
let shouldEmit = firstCall || value !== cache;
|
||||
firstCall = false;
|
||||
cache = value;
|
||||
return shouldEmit;
|
||||
});
|
||||
}
|
||||
|
||||
export function anyEvent<T>(...events: Event<T>[]): Event<T> {
|
||||
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
|
||||
const result = combinedDisposable(events.map(event => event(i => listener.call(thisArgs, i))));
|
||||
|
||||
@@ -425,10 +425,10 @@ https-proxy-agent@^2.2.1:
|
||||
agent-base "^4.3.0"
|
||||
debug "^3.1.0"
|
||||
|
||||
iconv-lite-umd@0.6.5:
|
||||
version "0.6.5"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite-umd/-/iconv-lite-umd-0.6.5.tgz#6a1f621a3b4d125f72feff813a9839e1ebd6c722"
|
||||
integrity sha512-WDegH4al+e3n3jTOStRvm+jzDA3JMUQGgzdAsMxAgcgB0Oi72HjfdsoX08ieKsy3rKexXVjWZr41aOIUaCZnMg==
|
||||
iconv-lite-umd@0.6.8:
|
||||
version "0.6.8"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite-umd/-/iconv-lite-umd-0.6.8.tgz#5ad310ec126b260621471a2d586f7f37b9958ec0"
|
||||
integrity sha512-zvXJ5gSwMC9JD3wDzH8CoZGc1pbiJn12Tqjk8BXYCnYz3hYL5GRjHW8LEykjXhV9WgNGI4rgpgHcbIiBfrRq6A==
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
@@ -468,10 +468,10 @@ jsbn@~0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
|
||||
|
||||
jschardet@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.1.1.tgz#af6f8fd0b3b0f5d46a8fd9614a4fce490575c184"
|
||||
integrity sha512-pA5qG9Zwm8CBpGlK/lo2GE9jPxwqRgMV7Lzc/1iaPccw6v4Rhj8Zg2BTyrdmHmxlJojnbLupLeRnaPLsq03x6Q==
|
||||
jschardet@2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.2.1.tgz#03b0264669a90c7a5c436a68c5a7d4e4cb0c9823"
|
||||
integrity sha512-Ks2JNuUJoc7PGaZ7bVFtSEvOcr0rBq6Q1J5/7+zKWLT+g+4zziL63O0jg7y2jxhzIa1LVsHUbPXrbaWmz9iwDw==
|
||||
|
||||
json-schema-traverse@^0.4.1:
|
||||
version "0.4.1"
|
||||
|
||||
@@ -24,19 +24,27 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
vscode.authentication.registerAuthenticationProvider({
|
||||
id: 'github',
|
||||
displayName: 'GitHub',
|
||||
label: 'GitHub',
|
||||
supportsMultipleAccounts: false,
|
||||
onDidChangeSessions: onDidChangeSessions.event,
|
||||
getSessions: () => Promise.resolve(loginService.sessions),
|
||||
login: async (scopeList: string[]) => {
|
||||
try {
|
||||
/* __GDPR__
|
||||
"login" : { }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('login');
|
||||
|
||||
const session = await loginService.login(scopeList.sort().join(' '));
|
||||
Logger.info('Login success!');
|
||||
onDidChangeSessions.fire({ added: [session.id], removed: [], changed: [] });
|
||||
return session;
|
||||
} catch (e) {
|
||||
/* __GDPR__
|
||||
"loginFailed" : { }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('loginFailed');
|
||||
|
||||
vscode.window.showErrorMessage(`Sign in failed: ${e}`);
|
||||
Logger.error(e);
|
||||
throw e;
|
||||
@@ -44,11 +52,19 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
},
|
||||
logout: async (id: string) => {
|
||||
try {
|
||||
/* __GDPR__
|
||||
"logout" : { }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('logout');
|
||||
|
||||
await loginService.logout(id);
|
||||
onDidChangeSessions.fire({ added: [], removed: [id], changed: [] });
|
||||
} catch (e) {
|
||||
/* __GDPR__
|
||||
"logoutFailed" : { }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('logoutFailed');
|
||||
|
||||
vscode.window.showErrorMessage(`Sign out failed: ${e}`);
|
||||
Logger.error(e);
|
||||
throw e;
|
||||
|
||||
@@ -14,7 +14,8 @@ export const onDidChangeSessions = new vscode.EventEmitter<vscode.Authentication
|
||||
interface SessionData {
|
||||
id: string;
|
||||
account?: {
|
||||
displayName: string;
|
||||
label?: string;
|
||||
displayName?: string;
|
||||
id: string;
|
||||
}
|
||||
scopes: string[];
|
||||
@@ -95,7 +96,9 @@ export class GitHubAuthenticationProvider {
|
||||
return {
|
||||
id: session.id,
|
||||
account: {
|
||||
displayName: session.account?.displayName ?? userInfo!.accountName,
|
||||
label: session.account
|
||||
? session.account.label || session.account.displayName!
|
||||
: userInfo!.accountName,
|
||||
id: session.account?.id ?? userInfo!.id
|
||||
},
|
||||
scopes: session.scopes,
|
||||
@@ -138,7 +141,7 @@ export class GitHubAuthenticationProvider {
|
||||
|
||||
private async tokenToSession(token: string, scopes: string[]): Promise<vscode.AuthenticationSession> {
|
||||
const userInfo = await this._githubServer.getUserInfo(token);
|
||||
return new vscode.AuthenticationSession(uuid(), token, { displayName: userInfo.accountName, id: userInfo.id }, scopes);
|
||||
return new vscode.AuthenticationSession(uuid(), token, { label: userInfo.accountName, id: userInfo.id }, scopes);
|
||||
}
|
||||
|
||||
private async setToken(session: vscode.AuthenticationSession): Promise<void> {
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "githubBrowser.openRepository",
|
||||
"title": "Open GitHub Repository...",
|
||||
"category": "GitHub Browser"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.commit",
|
||||
"title": "Commit",
|
||||
@@ -48,6 +53,10 @@
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "githubBrowser.openRepository",
|
||||
"when": "config.githubBrowser.openRepository"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.commit",
|
||||
"when": "false"
|
||||
|
||||
@@ -47,31 +47,17 @@ function fromSerialized(operations: StoredOperation): Operation {
|
||||
return { ...operations, uri: Uri.parse(operations.uri) };
|
||||
}
|
||||
|
||||
interface CreatedFileChangeStoreEvent {
|
||||
type: 'created';
|
||||
export interface ChangeStoreEvent {
|
||||
type: 'created' | 'changed' | 'deleted';
|
||||
rootUri: Uri;
|
||||
uri: Uri;
|
||||
}
|
||||
|
||||
interface ChangedFileChangeStoreEvent {
|
||||
type: 'changed';
|
||||
rootUri: Uri;
|
||||
uri: Uri;
|
||||
}
|
||||
|
||||
interface DeletedFileChangeStoreEvent {
|
||||
type: 'deleted';
|
||||
rootUri: Uri;
|
||||
uri: Uri;
|
||||
}
|
||||
|
||||
type ChangeStoreEvent = CreatedFileChangeStoreEvent | ChangedFileChangeStoreEvent | DeletedFileChangeStoreEvent;
|
||||
|
||||
function toChangeStoreEvent(operation: Operation | StoredOperation, rootUri: Uri, uri?: Uri): ChangeStoreEvent {
|
||||
return {
|
||||
type: operation.type,
|
||||
rootUri: rootUri,
|
||||
uri: uri ?? (typeof operation.uri === 'string' ? Uri.parse(operation.uri) : operation.uri)
|
||||
uri: uri ?? (typeof operation.uri === 'string' ? Uri.parse(operation.uri) : operation.uri),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,6 +68,8 @@ export interface IChangeStore {
|
||||
discard(uri: Uri): Promise<void>;
|
||||
discardAll(rootUri: Uri): Promise<void>;
|
||||
|
||||
hasChanges(rootUri: Uri): boolean;
|
||||
|
||||
getChanges(rootUri: Uri): Operation[];
|
||||
getContent(uri: Uri): string | undefined;
|
||||
|
||||
@@ -116,9 +104,15 @@ export class ChangeStore implements IChangeStore, IWritableChangeStore {
|
||||
|
||||
await this.saveWorkingOperations(rootUri, undefined);
|
||||
|
||||
const events: ChangeStoreEvent[] = [];
|
||||
|
||||
for (const operation of operations) {
|
||||
await this.discardWorkingContent(operation.uri);
|
||||
this._onDidChange.fire(toChangeStoreEvent(operation, rootUri));
|
||||
events.push(toChangeStoreEvent(operation, rootUri));
|
||||
}
|
||||
|
||||
for (const e of events) {
|
||||
this._onDidChange.fire(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +137,7 @@ export class ChangeStore implements IChangeStore, IWritableChangeStore {
|
||||
this._onDidChange.fire({
|
||||
type: operation.type === 'created' ? 'deleted' : operation.type === 'deleted' ? 'created' : 'changed',
|
||||
rootUri: rootUri,
|
||||
uri: uri
|
||||
uri: uri,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,9 +146,15 @@ export class ChangeStore implements IChangeStore, IWritableChangeStore {
|
||||
|
||||
await this.saveWorkingOperations(rootUri, undefined);
|
||||
|
||||
const events: ChangeStoreEvent[] = [];
|
||||
|
||||
for (const operation of operations) {
|
||||
await this.discardWorkingContent(operation.uri);
|
||||
this._onDidChange.fire(toChangeStoreEvent(operation, rootUri));
|
||||
events.push(toChangeStoreEvent(operation, rootUri));
|
||||
}
|
||||
|
||||
for (const e of events) {
|
||||
this._onDidChange.fire(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { Event, EventEmitter, Memento, Uri } from 'vscode';
|
||||
import { Event, EventEmitter, Memento, Uri, workspace } from 'vscode';
|
||||
|
||||
export const contextKeyPrefix = 'github.context|';
|
||||
export interface WorkspaceFolderContext<T> {
|
||||
context: T;
|
||||
name: string;
|
||||
folderUri: Uri;
|
||||
}
|
||||
|
||||
export class ContextStore<T> {
|
||||
private _onDidChange = new EventEmitter<Uri>();
|
||||
@@ -14,23 +18,36 @@ export class ContextStore<T> {
|
||||
return this._onDidChange.event;
|
||||
}
|
||||
|
||||
constructor(private readonly memento: Memento, private readonly scheme: string) { }
|
||||
constructor(
|
||||
private readonly scheme: string,
|
||||
private readonly originalScheme: string,
|
||||
private readonly memento: Memento,
|
||||
) { }
|
||||
|
||||
delete(uri: Uri) {
|
||||
return this.set(uri, undefined);
|
||||
}
|
||||
|
||||
get(uri: Uri): T | undefined {
|
||||
return this.memento.get<T>(`${contextKeyPrefix}${uri.toString()}`);
|
||||
return this.memento.get<T>(`${this.originalScheme}.context|${this.getOriginalResource(uri).toString()}`);
|
||||
}
|
||||
|
||||
getForWorkspace(): WorkspaceFolderContext<T>[] {
|
||||
const folders = workspace.workspaceFolders?.filter(f => f.uri.scheme === this.scheme || f.uri.scheme === this.originalScheme) ?? [];
|
||||
return folders.map(f => ({ context: this.get(f.uri)!, name: f.name, folderUri: f.uri })).filter(c => c.context !== undefined);
|
||||
}
|
||||
|
||||
async set(uri: Uri, context: T | undefined) {
|
||||
if (uri.scheme !== this.scheme) {
|
||||
throw new Error(`Invalid context scheme: ${uri.scheme}`);
|
||||
}
|
||||
|
||||
await this.memento.update(`${contextKeyPrefix}${uri.toString()}`, context);
|
||||
uri = this.getOriginalResource(uri);
|
||||
await this.memento.update(`${this.originalScheme}.context|${uri.toString()}`, context);
|
||||
this._onDidChange.fire(uri);
|
||||
}
|
||||
|
||||
getOriginalResource(uri: Uri): Uri {
|
||||
return uri.with({ scheme: this.originalScheme });
|
||||
}
|
||||
|
||||
getWorkspaceResource(uri: Uri): Uri {
|
||||
return uri.with({ scheme: this.scheme });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,48 +3,50 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ExtensionContext, Uri, workspace } from 'vscode';
|
||||
import { commands, ExtensionContext, Uri, window, workspace } from 'vscode';
|
||||
import { ChangeStore } from './changeStore';
|
||||
import { ContextStore } from './contextStore';
|
||||
import { VirtualFS } from './fs';
|
||||
import { GitHubApiContext, GitHubApi } from './github/api';
|
||||
import { GitHubFS } from './github/fs';
|
||||
import { VirtualSCM } from './scm';
|
||||
import { StatusBar } from './statusbar';
|
||||
|
||||
// const repositoryRegex = /^(?:(?:https:\/\/)?github.com\/)?([^\/]+)\/([^\/]+?)(?:\/|.git|$)/i;
|
||||
const repositoryRegex = /^(?:(?:https:\/\/)?github.com\/)?([^\/]+)\/([^\/]+?)(?:\/|.git|$)/i;
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
const contextStore = new ContextStore<GitHubApiContext>(context.workspaceState, GitHubFS.scheme);
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const contextStore = new ContextStore<GitHubApiContext>('codespace', GitHubFS.scheme, context.workspaceState);
|
||||
const changeStore = new ChangeStore(context.workspaceState);
|
||||
|
||||
const githubApi = new GitHubApi(contextStore);
|
||||
const gitHubFS = new GitHubFS(githubApi);
|
||||
const virtualFS = new VirtualFS('codespace', GitHubFS.scheme, contextStore, changeStore, gitHubFS);
|
||||
const virtualFS = new VirtualFS('codespace', contextStore, changeStore, gitHubFS);
|
||||
|
||||
context.subscriptions.push(
|
||||
githubApi,
|
||||
gitHubFS,
|
||||
virtualFS,
|
||||
new VirtualSCM(GitHubFS.scheme, githubApi, changeStore)
|
||||
new VirtualSCM(GitHubFS.scheme, githubApi, changeStore),
|
||||
new StatusBar(contextStore, changeStore),
|
||||
);
|
||||
|
||||
// commands.registerCommand('githubBrowser.openRepository', async () => {
|
||||
// const value = await window.showInputBox({
|
||||
// placeHolder: 'e.g. https://github.com/microsoft/vscode',
|
||||
// prompt: 'Enter a GitHub repository url',
|
||||
// validateInput: value => repositoryRegex.test(value) ? undefined : 'Invalid repository url'
|
||||
// });
|
||||
commands.registerCommand('githubBrowser.openRepository', async () => {
|
||||
const value = await window.showInputBox({
|
||||
placeHolder: 'e.g. https://github.com/microsoft/vscode',
|
||||
prompt: 'Enter a GitHub repository url',
|
||||
validateInput: value => repositoryRegex.test(value) ? undefined : 'Invalid repository url'
|
||||
});
|
||||
|
||||
// if (value) {
|
||||
// const match = repositoryRegex.exec(value);
|
||||
// if (match) {
|
||||
// const [, owner, repo] = match;
|
||||
if (value) {
|
||||
const match = repositoryRegex.exec(value);
|
||||
if (match) {
|
||||
const [, owner, repo] = match;
|
||||
|
||||
// const uri = Uri.parse(`codespace://HEAD/${owner}/${repo}`);
|
||||
// openWorkspace(uri, repo, 'currentWindow');
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
const uri = Uri.parse(`codespace://HEAD/${owner}/${repo}`);
|
||||
openWorkspace(uri, repo, 'currentWindow');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getRelativePath(rootUri: Uri, uri: Uri) {
|
||||
@@ -63,11 +65,16 @@ export function isDescendent(folderPath: string, filePath: string) {
|
||||
return folderPath.length === 0 || filePath.startsWith(folderPath.endsWith('/') ? folderPath : `${folderPath}/`);
|
||||
}
|
||||
|
||||
// function openWorkspace(uri: Uri, name: string, location: 'currentWindow' | 'newWindow' | 'addToCurrentWorkspace') {
|
||||
// if (location === 'addToCurrentWorkspace') {
|
||||
// const count = (workspace.workspaceFolders && workspace.workspaceFolders.length) || 0;
|
||||
// return workspace.updateWorkspaceFolders(count, 0, { uri: uri, name: name });
|
||||
// }
|
||||
const shaRegex = /^[0-9a-f]{40}$/;
|
||||
export function isSha(ref: string) {
|
||||
return shaRegex.test(ref);
|
||||
}
|
||||
|
||||
// return commands.executeCommand('vscode.openFolder', uri, location === 'newWindow');
|
||||
// }
|
||||
function openWorkspace(uri: Uri, name: string, location: 'currentWindow' | 'newWindow' | 'addToCurrentWorkspace') {
|
||||
if (location === 'addToCurrentWorkspace') {
|
||||
const count = (workspace.workspaceFolders && workspace.workspaceFolders.length) || 0;
|
||||
return workspace.updateWorkspaceFolders(count, 0, { uri: uri, name: name });
|
||||
}
|
||||
|
||||
return commands.executeCommand('vscode.openFolder', uri, location === 'newWindow');
|
||||
}
|
||||
|
||||
@@ -43,26 +43,22 @@ export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSe
|
||||
|
||||
constructor(
|
||||
readonly scheme: string,
|
||||
private readonly originalScheme: string,
|
||||
contextStore: ContextStore<GitHubApiContext>,
|
||||
private readonly contextStore: ContextStore<GitHubApiContext>,
|
||||
private readonly changeStore: IWritableChangeStore,
|
||||
private readonly fs: FileSystemProvider & FileSearchProvider & TextSearchProvider
|
||||
) {
|
||||
// TODO@eamodio listen for workspace folder changes
|
||||
for (const folder of workspace.workspaceFolders ?? []) {
|
||||
const uri = this.getOriginalResource(folder.uri);
|
||||
|
||||
for (const context of contextStore.getForWorkspace()) {
|
||||
// If we have a saved context, but no longer have any changes, reset the context
|
||||
// We only do this on startup/reload to keep things consistent
|
||||
if (contextStore.get(uri) !== undefined && !changeStore.hasChanges(folder.uri)) {
|
||||
contextStore.delete(uri);
|
||||
if (!changeStore.hasChanges(context.folderUri)) {
|
||||
console.log('Clear context', context.folderUri.toString());
|
||||
contextStore.delete(context.folderUri);
|
||||
}
|
||||
}
|
||||
|
||||
this.disposable = Disposable.from(
|
||||
workspace.registerFileSystemProvider(scheme, this, {
|
||||
isCaseSensitive: true,
|
||||
}),
|
||||
workspace.registerFileSystemProvider(scheme, this, { isCaseSensitive: true }),
|
||||
workspace.registerFileSearchProvider(scheme, this),
|
||||
workspace.registerTextSearchProvider(scheme, this),
|
||||
changeStore.onDidChange(e => {
|
||||
@@ -86,11 +82,11 @@ export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSe
|
||||
}
|
||||
|
||||
private getOriginalResource(uri: Uri): Uri {
|
||||
return uri.with({ scheme: this.originalScheme });
|
||||
return this.contextStore.getOriginalResource(uri);
|
||||
}
|
||||
|
||||
private getVirtualResource(uri: Uri): Uri {
|
||||
return uri.with({ scheme: this.scheme });
|
||||
private getWorkspaceResource(uri: Uri): Uri {
|
||||
return this.contextStore.getWorkspaceResource(uri);
|
||||
}
|
||||
|
||||
//#region FileSystemProvider
|
||||
@@ -211,7 +207,7 @@ export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSe
|
||||
return this.fs.provideTextSearchResults(
|
||||
query,
|
||||
{ ...options, folder: this.getOriginalResource(options.folder) },
|
||||
{ report: (result: TextSearchResult) => progress.report({ ...result, uri: this.getVirtualResource(result.uri) }) },
|
||||
{ report: (result: TextSearchResult) => progress.report({ ...result, uri: this.getWorkspaceResource(result.uri) }) },
|
||||
token
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@
|
||||
import { authentication, AuthenticationSession, Disposable, Event, EventEmitter, Range, Uri } from 'vscode';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { fromGitHubUri } from './fs';
|
||||
import { ContextStore } from '../contextStore';
|
||||
import { fromGitHubUri } from './fs';
|
||||
import { isSha } from '../extension';
|
||||
import { Iterables } from '../iterables';
|
||||
|
||||
export const shaRegex = /^[0-9a-f]{40}$/;
|
||||
|
||||
export interface GitHubApiContext {
|
||||
sha: string;
|
||||
requestRef: string;
|
||||
|
||||
branch: string;
|
||||
sha: string | undefined;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@@ -73,7 +75,7 @@ export class GitHubApi implements Disposable {
|
||||
if (!providers.includes('github')) {
|
||||
await new Promise(resolve => {
|
||||
authentication.onDidChangeAuthenticationProviders(e => {
|
||||
if (e.added.includes('github')) {
|
||||
if (e.added.find(provider => provider.id === 'github')) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
@@ -110,19 +112,12 @@ export class GitHubApi implements Disposable {
|
||||
}
|
||||
|
||||
async commit(rootUri: Uri, message: string, operations: CommitOperation[]): Promise<string | undefined> {
|
||||
let { owner, repo, ref } = fromGitHubUri(rootUri);
|
||||
const { owner, repo } = fromGitHubUri(rootUri);
|
||||
|
||||
try {
|
||||
if (ref === undefined || ref === 'HEAD') {
|
||||
ref = await this.defaultBranchQuery(rootUri);
|
||||
if (ref === undefined) {
|
||||
throw new Error('Cannot commit — invalid ref');
|
||||
}
|
||||
}
|
||||
|
||||
const context = await this.getContext(rootUri);
|
||||
if (context.sha === undefined) {
|
||||
throw new Error('Cannot commit — invalid context');
|
||||
throw new Error(`Cannot commit to Uri(${rootUri.toString(true)}); Invalid context sha`);
|
||||
}
|
||||
|
||||
const hasDeletes = operations.some(op => op.type === 'deleted');
|
||||
@@ -204,14 +199,14 @@ export class GitHubApi implements Disposable {
|
||||
parents: [context.sha]
|
||||
});
|
||||
|
||||
this.updateContext(rootUri, { sha: resp.data.sha, timestamp: Date.now() });
|
||||
this.updateContext(rootUri, { ...context, sha: resp.data.sha, timestamp: Date.now() });
|
||||
|
||||
// TODO@eamodio need to send a file change for any open files
|
||||
|
||||
await github.git.updateRef({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
ref: `heads/${ref}`,
|
||||
ref: `heads/${context.branch}`,
|
||||
sha: resp.data.sha
|
||||
});
|
||||
|
||||
@@ -256,7 +251,7 @@ export class GitHubApi implements Disposable {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
recursive: '1',
|
||||
tree_sha: context?.sha ?? ref ?? 'HEAD',
|
||||
tree_sha: context?.sha ?? ref,
|
||||
});
|
||||
return Iterables.filterMap(resp.data.tree, p => p.type === 'blob' ? p.path : undefined);
|
||||
} catch (ex) {
|
||||
@@ -283,7 +278,7 @@ export class GitHubApi implements Disposable {
|
||||
}>(query, {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
path: `${context.sha ?? ref ?? 'HEAD'}:${path}`,
|
||||
path: `${context.sha ?? ref}:${path}`,
|
||||
});
|
||||
return rsp?.repository?.object ?? undefined;
|
||||
} catch (ex) {
|
||||
@@ -295,7 +290,7 @@ export class GitHubApi implements Disposable {
|
||||
const { owner, repo, ref } = fromGitHubUri(uri);
|
||||
|
||||
try {
|
||||
if (ref === undefined || ref === 'HEAD') {
|
||||
if (ref === 'HEAD') {
|
||||
const query = `query latest($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
defaultBranchRef {
|
||||
@@ -322,6 +317,7 @@ export class GitHubApi implements Disposable {
|
||||
oid
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const rsp = await this.gqlQuery<{
|
||||
@@ -345,7 +341,7 @@ export class GitHubApi implements Disposable {
|
||||
const { owner, repo, ref } = fromGitHubUri(uri);
|
||||
|
||||
// If we have a specific ref, don't try to search, because GitHub search only works against the default branch
|
||||
if (ref === undefined) {
|
||||
if (ref !== 'HEAD') {
|
||||
return { matches: [], limitHit: true };
|
||||
}
|
||||
|
||||
@@ -436,29 +432,46 @@ export class GitHubApi implements Disposable {
|
||||
private readonly rootUriToContextMap = new Map<string, GitHubApiContext>();
|
||||
|
||||
private async getContextCore(rootUri: Uri): Promise<GitHubApiContext> {
|
||||
let context = this.rootUriToContextMap.get(rootUri.toString());
|
||||
if (context === undefined) {
|
||||
const { ref } = fromGitHubUri(rootUri);
|
||||
if (ref !== undefined && shaRegex.test(ref)) {
|
||||
context = { sha: ref, timestamp: Date.now() };
|
||||
} else {
|
||||
context = this.context.get(rootUri);
|
||||
if (context?.sha === undefined) {
|
||||
const sha = await this.latestCommitQuery(rootUri);
|
||||
if (sha !== undefined) {
|
||||
context = { sha: sha, timestamp: Date.now() };
|
||||
} else {
|
||||
context = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
const key = rootUri.toString();
|
||||
let context = this.rootUriToContextMap.get(key);
|
||||
|
||||
if (context !== undefined) {
|
||||
this.updateContext(rootUri, context);
|
||||
}
|
||||
// Check if we have a cached a context
|
||||
if (context?.sha !== undefined) {
|
||||
return context;
|
||||
}
|
||||
|
||||
return context ?? { sha: rootUri.authority, timestamp: Date.now() };
|
||||
// Check if we have a saved context
|
||||
context = this.context.get(rootUri);
|
||||
if (context?.sha !== undefined) {
|
||||
this.rootUriToContextMap.set(key, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const { ref } = fromGitHubUri(rootUri);
|
||||
|
||||
// If the requested ref looks like a sha, then use it
|
||||
if (isSha(ref)) {
|
||||
context = { requestRef: ref, branch: ref, sha: ref, timestamp: Date.now() };
|
||||
} else {
|
||||
let branch;
|
||||
if (ref === 'HEAD') {
|
||||
branch = await this.defaultBranchQuery(rootUri);
|
||||
if (branch === undefined) {
|
||||
throw new Error(`Cannot get context for Uri(${rootUri.toString(true)}); unable to get default branch`);
|
||||
}
|
||||
} else {
|
||||
branch = ref;
|
||||
}
|
||||
|
||||
// Query for the latest sha for the give ref
|
||||
const sha = await this.latestCommitQuery(rootUri);
|
||||
context = { requestRef: ref, branch: branch, sha: sha, timestamp: Date.now() };
|
||||
}
|
||||
|
||||
this.updateContext(rootUri, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private updateContext(rootUri: Uri, context: GitHubApiContext) {
|
||||
|
||||
@@ -299,7 +299,7 @@ function typenameToFileType(typename: string | undefined | null) {
|
||||
}
|
||||
}
|
||||
|
||||
type RepoInfo = { owner: string; repo: string; path: string | undefined; ref?: string };
|
||||
type RepoInfo = { owner: string; repo: string; path: string | undefined; ref: string };
|
||||
export function fromGitHubUri(uri: Uri): RepoInfo {
|
||||
const [, owner, repo, ...rest] = uri.path.split('/');
|
||||
|
||||
@@ -311,7 +311,7 @@ export function fromGitHubUri(uri: Uri): RepoInfo {
|
||||
ref = 'HEAD';
|
||||
}
|
||||
}
|
||||
return { owner: owner, repo: repo, path: rest.join('/'), ref: ref };
|
||||
return { owner: owner, repo: repo, path: rest.join('/'), ref: ref ?? 'HEAD' };
|
||||
}
|
||||
|
||||
function getHashCode(s: string): number {
|
||||
|
||||
@@ -12,8 +12,9 @@ export namespace Iterables {
|
||||
): Iterable<TMapped> {
|
||||
for (const item of source) {
|
||||
const mapped = predicateMapper(item);
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (mapped != null) { yield mapped; }
|
||||
if (mapped !== undefined && mapped !== null) {
|
||||
yield mapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,17 +32,15 @@ export class VirtualSCM implements Disposable {
|
||||
// TODO@eamodio listen for workspace folder changes
|
||||
for (const folder of workspace.workspaceFolders ?? []) {
|
||||
this.createScmProvider(folder.uri, folder.name);
|
||||
|
||||
for (const operation of changeStore.getChanges(folder.uri)) {
|
||||
this.update(folder.uri, operation.uri);
|
||||
}
|
||||
}
|
||||
|
||||
this.disposable = Disposable.from(
|
||||
changeStore.onDidChange(e => this.update(e.rootUri, e.uri)),
|
||||
);
|
||||
|
||||
for (const { uri } of workspace.workspaceFolders ?? []) {
|
||||
for (const operation of changeStore.getChanges(uri)) {
|
||||
this.update(uri, operation.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
@@ -50,7 +48,18 @@ export class VirtualSCM implements Disposable {
|
||||
}
|
||||
|
||||
private registerCommands() {
|
||||
commands.registerCommand('githubBrowser.commit', (...args: any[]) => this.commitChanges(args[0]));
|
||||
commands.registerCommand('githubBrowser.commit', (sourceControl: SourceControl | undefined) => {
|
||||
// TODO@eamodio remove this hack once I figure out why the args are missing
|
||||
if (sourceControl === undefined && this.providers.length === 1) {
|
||||
sourceControl = this.providers[0].sourceControl;
|
||||
}
|
||||
|
||||
if (sourceControl === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.commitChanges(sourceControl);
|
||||
});
|
||||
|
||||
commands.registerCommand('githubBrowser.discardChanges', (resourceState: SourceControlResourceState) =>
|
||||
this.discardChanges(resourceState.resourceUri)
|
||||
|
||||
99
extensions/github-browser/src/statusbar.ts
Normal file
99
extensions/github-browser/src/statusbar.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { Disposable, StatusBarAlignment, StatusBarItem, Uri, window, workspace } from 'vscode';
|
||||
import { ChangeStoreEvent, IChangeStore } from './changeStore';
|
||||
import { GitHubApiContext } from './github/api';
|
||||
import { isSha } from './extension';
|
||||
import { ContextStore, WorkspaceFolderContext } from './contextStore';
|
||||
|
||||
export class StatusBar implements Disposable {
|
||||
private readonly disposable: Disposable;
|
||||
|
||||
private readonly items = new Map<string, StatusBarItem>();
|
||||
|
||||
constructor(
|
||||
private readonly contextStore: ContextStore<GitHubApiContext>,
|
||||
private readonly changeStore: IChangeStore
|
||||
) {
|
||||
this.disposable = Disposable.from(
|
||||
contextStore.onDidChange(this.onContextsChanged, this),
|
||||
changeStore.onDidChange(this.onChanged, this)
|
||||
);
|
||||
|
||||
for (const context of this.contextStore.getForWorkspace()) {
|
||||
this.createOrUpdateStatusBarItem(context);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable?.dispose();
|
||||
this.items.forEach(i => i.dispose());
|
||||
}
|
||||
|
||||
private createOrUpdateStatusBarItem(wc: WorkspaceFolderContext<GitHubApiContext>) {
|
||||
let item = this.items.get(wc.folderUri.toString());
|
||||
if (item === undefined) {
|
||||
item = window.createStatusBarItem({
|
||||
id: `githubBrowser.branch:${wc.folderUri.toString()}`,
|
||||
name: `GitHub Browser: ${wc.name}`,
|
||||
alignment: StatusBarAlignment.Left,
|
||||
priority: 1000
|
||||
});
|
||||
}
|
||||
|
||||
if (isSha(wc.context.branch)) {
|
||||
item.text = `$(git-commit) ${wc.context.branch.substr(0, 8)}`;
|
||||
item.tooltip = `${wc.name} \u2022 ${wc.context.branch.substr(0, 8)}`;
|
||||
} else {
|
||||
item.text = `$(git-branch) ${wc.context.branch}`;
|
||||
item.tooltip = `${wc.name} \u2022 ${wc.context.branch}${wc.context.sha ? ` @ ${wc.context.sha?.substr(0, 8)}` : ''}`;
|
||||
}
|
||||
|
||||
const hasChanges = this.changeStore.hasChanges(wc.folderUri);
|
||||
if (hasChanges) {
|
||||
item.text += '*';
|
||||
}
|
||||
|
||||
item.show();
|
||||
|
||||
this.items.set(wc.folderUri.toString(), item);
|
||||
}
|
||||
|
||||
private onContextsChanged(uri: Uri) {
|
||||
const folder = workspace.getWorkspaceFolder(this.contextStore.getWorkspaceResource(uri));
|
||||
if (folder === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = this.contextStore.get(uri);
|
||||
if (context === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.createOrUpdateStatusBarItem({
|
||||
context: context,
|
||||
name: folder.name,
|
||||
folderUri: folder.uri,
|
||||
});
|
||||
}
|
||||
|
||||
private onChanged(e: ChangeStoreEvent) {
|
||||
const item = this.items.get(e.rootUri.toString());
|
||||
if (item !== undefined) {
|
||||
const hasChanges = this.changeStore.hasChanges(e.rootUri);
|
||||
if (hasChanges) {
|
||||
if (!item.text.endsWith('*')) {
|
||||
item.text += '*';
|
||||
}
|
||||
} else {
|
||||
if (item.text.endsWith('*')) {
|
||||
item.text = item.text.substr(0, item.text.length - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,10 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { API as GitAPI } from './typings/git';
|
||||
import { publishRepository } from './publish';
|
||||
import { combinedDisposable } from './util';
|
||||
|
||||
export function registerCommands(gitAPI: GitAPI): vscode.Disposable[] {
|
||||
const disposables = [];
|
||||
export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
|
||||
const disposables: vscode.Disposable[] = [];
|
||||
|
||||
disposables.push(vscode.commands.registerCommand('github.publish', async () => {
|
||||
try {
|
||||
@@ -18,5 +19,5 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable[] {
|
||||
}
|
||||
}));
|
||||
|
||||
return disposables;
|
||||
return combinedDisposable(disposables);
|
||||
}
|
||||
|
||||
@@ -3,23 +3,43 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Disposable, ExtensionContext, extensions } from 'vscode';
|
||||
import { GithubRemoteSourceProvider } from './remoteSourceProvider';
|
||||
import { GitExtension } from './typings/git';
|
||||
import { registerCommands } from './commands';
|
||||
import { GithubCredentialProviderManager } from './credentialProvider';
|
||||
import { dispose, combinedDisposable } from './util';
|
||||
import { GithubPushErrorHandler } from './pushErrorHandler';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
const gitExtension = vscode.extensions.getExtension<GitExtension>('vscode.git')!.exports;
|
||||
export function activate(context: ExtensionContext): void {
|
||||
const disposables = new Set<Disposable>();
|
||||
context.subscriptions.push(combinedDisposable(disposables));
|
||||
|
||||
try {
|
||||
const gitAPI = gitExtension.getAPI(1);
|
||||
const init = () => {
|
||||
try {
|
||||
const gitAPI = gitExtension.getAPI(1);
|
||||
|
||||
context.subscriptions.push(...registerCommands(gitAPI));
|
||||
context.subscriptions.push(gitAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider(gitAPI)));
|
||||
context.subscriptions.push(new GithubCredentialProviderManager(gitAPI));
|
||||
} catch (err) {
|
||||
console.error('Could not initialize GitHub extension');
|
||||
console.warn(err);
|
||||
}
|
||||
disposables.add(registerCommands(gitAPI));
|
||||
disposables.add(gitAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider(gitAPI)));
|
||||
disposables.add(new GithubCredentialProviderManager(gitAPI));
|
||||
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler()));
|
||||
} catch (err) {
|
||||
console.error('Could not initialize GitHub extension');
|
||||
console.warn(err);
|
||||
}
|
||||
};
|
||||
|
||||
const onDidChangeGitExtensionEnablement = (enabled: boolean) => {
|
||||
if (!enabled) {
|
||||
dispose(disposables);
|
||||
disposables.clear();
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const gitExtension = extensions.getExtension<GitExtension>('vscode.git')!.exports;
|
||||
context.subscriptions.push(gitExtension.onDidChangeEnablement(onDidChangeGitExtensionEnablement));
|
||||
onDidChangeGitExtensionEnablement(gitExtension.enabled);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import * as path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import { API as GitAPI, Repository } from './typings/git';
|
||||
import { getOctokit } from './auth';
|
||||
import { TextEncoder } from 'util';
|
||||
import { basename } from 'path';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -28,10 +28,12 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
|
||||
return;
|
||||
}
|
||||
|
||||
let folder: vscode.WorkspaceFolder;
|
||||
let folder: vscode.Uri;
|
||||
|
||||
if (vscode.workspace.workspaceFolders.length === 1) {
|
||||
folder = vscode.workspace.workspaceFolders[0];
|
||||
if (repository) {
|
||||
folder = repository.rootUri;
|
||||
} else if (vscode.workspace.workspaceFolders.length === 1) {
|
||||
folder = vscode.workspace.workspaceFolders[0].uri;
|
||||
} else {
|
||||
const picks = vscode.workspace.workspaceFolders.map(folder => ({ label: folder.name, folder }));
|
||||
const placeHolder = localize('pick folder', "Pick a folder to publish to GitHub");
|
||||
@@ -41,14 +43,14 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
|
||||
return;
|
||||
}
|
||||
|
||||
folder = pick.folder;
|
||||
folder = pick.folder.uri;
|
||||
}
|
||||
|
||||
let quickpick = vscode.window.createQuickPick<vscode.QuickPickItem & { repo?: string, auth?: 'https' | 'ssh' }>();
|
||||
quickpick.ignoreFocusOut = true;
|
||||
|
||||
quickpick.placeholder = 'Repository Name';
|
||||
quickpick.value = folder.name;
|
||||
quickpick.value = basename(folder.fsPath);
|
||||
quickpick.show();
|
||||
quickpick.busy = true;
|
||||
|
||||
@@ -97,37 +99,49 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
|
||||
return;
|
||||
}
|
||||
|
||||
quickpick = vscode.window.createQuickPick();
|
||||
quickpick.placeholder = localize('ignore', "Select which files should be included in the repository.");
|
||||
quickpick.canSelectMany = true;
|
||||
quickpick.show();
|
||||
if (!repository) {
|
||||
const gitignore = vscode.Uri.joinPath(folder, '.gitignore');
|
||||
let shouldGenerateGitignore = false;
|
||||
|
||||
try {
|
||||
quickpick.busy = true;
|
||||
|
||||
const repositoryPath = folder.uri.fsPath;
|
||||
const currentPath = path.join(repositoryPath);
|
||||
const children = await fs.readdir(currentPath);
|
||||
quickpick.items = children.map(name => ({ label: name }));
|
||||
quickpick.selectedItems = quickpick.items;
|
||||
quickpick.busy = false;
|
||||
|
||||
const result = await Promise.race([
|
||||
new Promise<readonly vscode.QuickPickItem[]>(c => quickpick.onDidAccept(() => c(quickpick.selectedItems))),
|
||||
new Promise<undefined>(c => quickpick.onDidHide(() => c(undefined)))
|
||||
]);
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
try {
|
||||
await vscode.workspace.fs.stat(gitignore);
|
||||
} catch (err) {
|
||||
shouldGenerateGitignore = true;
|
||||
}
|
||||
|
||||
const ignored = new Set(children);
|
||||
result.forEach(c => ignored.delete(c.label));
|
||||
if (shouldGenerateGitignore) {
|
||||
quickpick = vscode.window.createQuickPick();
|
||||
quickpick.placeholder = localize('ignore', "Select which files should be included in the repository.");
|
||||
quickpick.canSelectMany = true;
|
||||
quickpick.show();
|
||||
|
||||
const raw = [...ignored].map(i => `/${i}`).join('\n');
|
||||
await fs.writeFile(path.join(repositoryPath, '.gitignore'), raw, 'utf8');
|
||||
} finally {
|
||||
quickpick.dispose();
|
||||
try {
|
||||
quickpick.busy = true;
|
||||
|
||||
const children = (await vscode.workspace.fs.readDirectory(folder)).map(([name]) => name);
|
||||
quickpick.items = children.map(name => ({ label: name }));
|
||||
quickpick.selectedItems = quickpick.items;
|
||||
quickpick.busy = false;
|
||||
|
||||
const result = await Promise.race([
|
||||
new Promise<readonly vscode.QuickPickItem[]>(c => quickpick.onDidAccept(() => c(quickpick.selectedItems))),
|
||||
new Promise<undefined>(c => quickpick.onDidHide(() => c(undefined)))
|
||||
]);
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ignored = new Set(children);
|
||||
result.forEach(c => ignored.delete(c.label));
|
||||
|
||||
const raw = [...ignored].map(i => `/${i}`).join('\n');
|
||||
const encoder = new TextEncoder();
|
||||
await vscode.workspace.fs.writeFile(gitignore, encoder.encode(raw));
|
||||
} finally {
|
||||
quickpick.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const githubRepository = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: false, title: 'Publish to GitHub' }, async progress => {
|
||||
@@ -143,7 +157,7 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
|
||||
progress.report({ message: 'Creating first commit', increment: 25 });
|
||||
|
||||
if (!repository) {
|
||||
repository = await gitAPI.init(folder.uri) || undefined;
|
||||
repository = await gitAPI.init(folder) || undefined;
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
|
||||
120
extensions/github/src/pushErrorHandler.ts
Normal file
120
extensions/github/src/pushErrorHandler.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { PushErrorHandler, GitErrorCodes, Repository, Remote } from './typings/git';
|
||||
import { window, ProgressLocation, commands, Uri } from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { getOctokit } from './auth';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
async function handlePushError(repository: Repository, remote: Remote, refspec: string, owner: string, repo: string): Promise<void> {
|
||||
const yes = localize('create a fork', "Create Fork");
|
||||
const no = localize('no', "No");
|
||||
|
||||
const answer = await window.showInformationMessage(localize('fork', "You don't have permissions to push to '{0}/{1}' on GitHub. Would you like to create a fork and push to it instead?", owner, repo), yes, no);
|
||||
|
||||
if (answer === no) {
|
||||
return;
|
||||
}
|
||||
|
||||
const match = /^([^:]*):([^:]*)$/.exec(refspec);
|
||||
const localName = match ? match[1] : refspec;
|
||||
const remoteName = match ? match[2] : refspec;
|
||||
|
||||
const [octokit, ghRepository] = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: localize('create fork', 'Create GitHub fork') }, async progress => {
|
||||
progress.report({ message: localize('forking', "Forking '{0}/{1}'...", owner, repo), increment: 33 });
|
||||
|
||||
const octokit = await getOctokit();
|
||||
|
||||
// Issue: what if the repo already exists?
|
||||
const res = await octokit.repos.createFork({ owner, repo });
|
||||
const ghRepository = res.data;
|
||||
|
||||
progress.report({ message: localize('pushing', "Pushing changes..."), increment: 33 });
|
||||
|
||||
// Issue: what if there's already an `upstream` repo?
|
||||
await repository.renameRemote(remote.name, 'upstream');
|
||||
|
||||
// Issue: what if there's already another `origin` repo?
|
||||
await repository.addRemote('origin', ghRepository.clone_url);
|
||||
await repository.fetch('origin', remoteName);
|
||||
await repository.setBranchUpstream(localName, `origin/${remoteName}`);
|
||||
await repository.push('origin', localName, true);
|
||||
|
||||
return [octokit, ghRepository];
|
||||
});
|
||||
|
||||
// yield
|
||||
(async () => {
|
||||
const openInGitHub = localize('openingithub', "Open In GitHub");
|
||||
const createPR = localize('createpr', "Create PR");
|
||||
const action = await window.showInformationMessage(localize('done', "The fork '{0}' was successfully created on GitHub.", ghRepository.full_name), openInGitHub, createPR);
|
||||
|
||||
if (action === openInGitHub) {
|
||||
await commands.executeCommand('vscode.open', Uri.parse(ghRepository.html_url));
|
||||
} else if (action === createPR) {
|
||||
const pr = await window.withProgress({ location: ProgressLocation.Notification, cancellable: false, title: localize('createghpr', "Creating GitHub Pull Request...") }, async _ => {
|
||||
let title = `Update ${remoteName}`;
|
||||
const head = repository.state.HEAD?.name;
|
||||
|
||||
if (head) {
|
||||
const commit = await repository.getCommit(head);
|
||||
title = commit.message.replace(/\n.*$/m, '');
|
||||
}
|
||||
|
||||
const res = await octokit.pulls.create({
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
head: `${ghRepository.owner.login}:${remoteName}`,
|
||||
base: remoteName
|
||||
});
|
||||
|
||||
await repository.setConfig(`branch.${localName}.remote`, 'upstream');
|
||||
await repository.setConfig(`branch.${localName}.merge`, `refs/heads/${remoteName}`);
|
||||
await repository.setConfig(`branch.${localName}.github-pr-owner-number`, `${owner}#${repo}#${pr.number}`);
|
||||
|
||||
return res.data;
|
||||
});
|
||||
|
||||
const openPR = localize('openpr', "Open PR");
|
||||
const action = await window.showInformationMessage(localize('donepr', "The PR '{0}/{1}#{2}' was successfully created on GitHub.", owner, repo, pr.number), openPR);
|
||||
|
||||
if (action === openPR) {
|
||||
await commands.executeCommand('vscode.open', Uri.parse(pr.html_url));
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
export class GithubPushErrorHandler implements PushErrorHandler {
|
||||
|
||||
async handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean> {
|
||||
if (error.gitErrorCode !== GitErrorCodes.PermissionDenied) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!remote.pushUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\.git/i.exec(remote.pushUrl)
|
||||
|| /^git@github\.com:([^/]+)\/([^/]+)\.git/i.exec(remote.pushUrl);
|
||||
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/^:/.test(refspec)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [, owner, repo] = match;
|
||||
await handlePushError(repository, remote, refspec, owner, repo);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
8
extensions/github/src/typings/git.d.ts
vendored
8
extensions/github/src/typings/git.d.ts
vendored
@@ -134,6 +134,8 @@ export interface CommitOptions {
|
||||
|
||||
export interface BranchQuery {
|
||||
readonly remote?: boolean;
|
||||
readonly pattern?: string;
|
||||
readonly count?: number;
|
||||
readonly contains?: string;
|
||||
}
|
||||
|
||||
@@ -221,6 +223,10 @@ export interface CredentialsProvider {
|
||||
getCredentials(host: Uri): ProviderResult<Credentials>;
|
||||
}
|
||||
|
||||
export interface PushErrorHandler {
|
||||
handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean>;
|
||||
}
|
||||
|
||||
export type APIState = 'uninitialized' | 'initialized';
|
||||
|
||||
export interface API {
|
||||
@@ -237,6 +243,7 @@ export interface API {
|
||||
|
||||
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
|
||||
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
|
||||
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
|
||||
}
|
||||
|
||||
export interface GitExtension {
|
||||
@@ -274,6 +281,7 @@ export const enum GitErrorCodes {
|
||||
CantOpenResource = 'CantOpenResource',
|
||||
GitNotFound = 'GitNotFound',
|
||||
CantCreatePipe = 'CantCreatePipe',
|
||||
PermissionDenied = 'PermissionDenied',
|
||||
CantAccessRemote = 'CantAccessRemote',
|
||||
RepositoryNotFound = 'RepositoryNotFound',
|
||||
RepositoryIsLocked = 'RepositoryIsLocked',
|
||||
|
||||
24
extensions/github/src/util.ts
Normal file
24
extensions/github/src/util.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function dispose(arg: vscode.Disposable | Iterable<vscode.Disposable>): void {
|
||||
if (arg instanceof vscode.Disposable) {
|
||||
arg.dispose();
|
||||
} else {
|
||||
for (const disposable of arg) {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function combinedDisposable(disposables: Iterable<vscode.Disposable>): vscode.Disposable {
|
||||
return {
|
||||
dispose() {
|
||||
dispose(disposables);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -48,14 +48,14 @@
|
||||
"JSON with Comments"
|
||||
],
|
||||
"extensions": [
|
||||
".hintrc",
|
||||
".babelrc",
|
||||
".jsonc",
|
||||
".eslintrc",
|
||||
".eslintrc.json",
|
||||
".jsfmtrc",
|
||||
".jshintrc",
|
||||
".swcrc"
|
||||
".swcrc",
|
||||
".hintrc",
|
||||
".babelrc"
|
||||
],
|
||||
"configuration": "./language-configuration.json"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"If you want to provide a fix or improvement, please create a pull request against the original repository.",
|
||||
"Once accepted there, we are happy to receive an update request."
|
||||
],
|
||||
"version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/a7e4475626a505472c76d18e0a1b3cfcf46f9cf9",
|
||||
"version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/4be9cb335581f3559166c319607dac9100103083",
|
||||
"name": "Markdown",
|
||||
"scopeName": "text.html.markdown",
|
||||
"patterns": [
|
||||
@@ -1963,12 +1963,12 @@
|
||||
"name": "markup.fenced_code.block.markdown"
|
||||
},
|
||||
"heading": {
|
||||
"match": "(?:^|\\G)[ ]{0,3}((#{1,6})\\s+(?=[\\S[^#]]).*?\\s*(#{1,6})?)$\\n?",
|
||||
"match": "(?:^|\\G)[ ]{0,3}(#{1,6}\\s+(.*?)(\\s+#{1,6})?\\s*)$",
|
||||
"captures": {
|
||||
"1": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "(#{6})\\s+(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?",
|
||||
"match": "(#{6})\\s+(.*?)(?:\\s+(#+))?\\s*$",
|
||||
"name": "heading.6.markdown",
|
||||
"captures": {
|
||||
"1": {
|
||||
@@ -1983,7 +1983,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "(#{5})\\s+(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?",
|
||||
"match": "(#{5})\\s+(.*?)(?:\\s+(#+))?\\s*$",
|
||||
"name": "heading.5.markdown",
|
||||
"captures": {
|
||||
"1": {
|
||||
@@ -1998,7 +1998,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "(#{4})\\s+(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?",
|
||||
"match": "(#{4})\\s+(.*?)(?:\\s+(#+))?\\s*$",
|
||||
"name": "heading.4.markdown",
|
||||
"captures": {
|
||||
"1": {
|
||||
@@ -2013,7 +2013,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "(#{3})\\s+(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?",
|
||||
"match": "(#{3})\\s+(.*?)(?:\\s+(#+))?\\s*$",
|
||||
"name": "heading.3.markdown",
|
||||
"captures": {
|
||||
"1": {
|
||||
@@ -2028,7 +2028,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "(#{2})\\s+(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?",
|
||||
"match": "(#{2})\\s+(.*?)(?:\\s+(#+))?\\s*$",
|
||||
"name": "heading.2.markdown",
|
||||
"captures": {
|
||||
"1": {
|
||||
@@ -2043,7 +2043,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "(#{1})\\s+(?=[\\S[^#]])(.*?)\\s*(\\s+#+)?$\\n?",
|
||||
"match": "(#{1})\\s+(.*?)(?:\\s+(#+))?\\s*$",
|
||||
"name": "heading.1.markdown",
|
||||
"captures": {
|
||||
"1": {
|
||||
|
||||
@@ -33,7 +33,18 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"c": " #",
|
||||
"c": " ",
|
||||
"t": "text.html.markdown markup.heading.markdown heading.1.markdown",
|
||||
"r": {
|
||||
"dark_plus": "markup.heading: #569CD6",
|
||||
"light_plus": "markup.heading: #800000",
|
||||
"dark_vs": "markup.heading: #569CD6",
|
||||
"light_vs": "markup.heading: #800000",
|
||||
"hc_black": "markup.heading: #6796E6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"c": "#",
|
||||
"t": "text.html.markdown markup.heading.markdown heading.1.markdown punctuation.definition.heading.markdown",
|
||||
"r": {
|
||||
"dark_plus": "markup.heading: #569CD6",
|
||||
@@ -77,7 +88,18 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"c": " ##",
|
||||
"c": " ",
|
||||
"t": "text.html.markdown markup.heading.markdown heading.2.markdown",
|
||||
"r": {
|
||||
"dark_plus": "markup.heading: #569CD6",
|
||||
"light_plus": "markup.heading: #800000",
|
||||
"dark_vs": "markup.heading: #569CD6",
|
||||
"light_vs": "markup.heading: #800000",
|
||||
"hc_black": "markup.heading: #6796E6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"c": "##",
|
||||
"t": "text.html.markdown markup.heading.markdown heading.2.markdown punctuation.definition.heading.markdown",
|
||||
"r": {
|
||||
"dark_plus": "markup.heading: #569CD6",
|
||||
@@ -2189,7 +2211,18 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"c": " ##",
|
||||
"c": " ",
|
||||
"t": "text.html.markdown markup.heading.markdown heading.2.markdown",
|
||||
"r": {
|
||||
"dark_plus": "markup.heading: #569CD6",
|
||||
"light_plus": "markup.heading: #800000",
|
||||
"dark_vs": "markup.heading: #569CD6",
|
||||
"light_vs": "markup.heading: #800000",
|
||||
"hc_black": "markup.heading: #6796E6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"c": "##",
|
||||
"t": "text.html.markdown markup.heading.markdown heading.2.markdown punctuation.definition.heading.markdown",
|
||||
"r": {
|
||||
"dark_plus": "markup.heading: #569CD6",
|
||||
@@ -2343,7 +2376,18 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"c": " ##",
|
||||
"c": " ",
|
||||
"t": "text.html.markdown markup.heading.markdown heading.2.markdown",
|
||||
"r": {
|
||||
"dark_plus": "markup.heading: #569CD6",
|
||||
"light_plus": "markup.heading: #800000",
|
||||
"dark_vs": "markup.heading: #569CD6",
|
||||
"light_vs": "markup.heading: #800000",
|
||||
"hc_black": "markup.heading: #6796E6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"c": "##",
|
||||
"t": "text.html.markdown markup.heading.markdown heading.2.markdown punctuation.definition.heading.markdown",
|
||||
"r": {
|
||||
"dark_plus": "markup.heading: #569CD6",
|
||||
|
||||
@@ -11,6 +11,21 @@ html, body {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
/* Reset margin top for elements */
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
p, ol, ul, pre {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h2, h3, h4, h5, h6 {
|
||||
font-weight: normal;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
#code-csp-warning {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -112,6 +127,20 @@ textarea:focus {
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
/* don't space 2 paragraphs too far apart */
|
||||
p + p {
|
||||
margin-top: -0.8em;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 2px;
|
||||
@@ -123,9 +152,6 @@ h1 {
|
||||
line-height: 1.2;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"editor/title": [
|
||||
{
|
||||
"command": "markdown.showPreviewToSide",
|
||||
"when": "editorLangId == markdown",
|
||||
"when": "editorLangId == markdown && !notebookEditorFocused",
|
||||
"alt": "markdown.showPreview",
|
||||
"group": "navigation"
|
||||
},
|
||||
@@ -115,23 +115,23 @@
|
||||
{
|
||||
"command": "markdown.showPreview",
|
||||
"when": "resourceLangId == markdown",
|
||||
"group": "navigation"
|
||||
"group": "1_open"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "markdown.showPreview",
|
||||
"when": "editorLangId == markdown",
|
||||
"when": "editorLangId == markdown && !notebookEditorFocused",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "markdown.showPreviewToSide",
|
||||
"when": "editorLangId == markdown",
|
||||
"when": "editorLangId == markdown && !notebookEditorFocused",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "markdown.showLockedPreviewToSide",
|
||||
"when": "editorLangId == markdown",
|
||||
"when": "editorLangId == markdown && !notebookEditorFocused",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
@@ -141,7 +141,7 @@
|
||||
},
|
||||
{
|
||||
"command": "markdown.showPreviewSecuritySelector",
|
||||
"when": "editorLangId == markdown"
|
||||
"when": "editorLangId == markdown && !notebookEditorFocused"
|
||||
},
|
||||
{
|
||||
"command": "markdown.showPreviewSecuritySelector",
|
||||
@@ -153,7 +153,7 @@
|
||||
},
|
||||
{
|
||||
"command": "markdown.preview.refresh",
|
||||
"when": "editorLangId == markdown"
|
||||
"when": "editorLangId == markdown && !notebookEditorFocused"
|
||||
},
|
||||
{
|
||||
"command": "markdown.preview.refresh",
|
||||
@@ -166,13 +166,13 @@
|
||||
"command": "markdown.showPreview",
|
||||
"key": "shift+ctrl+v",
|
||||
"mac": "shift+cmd+v",
|
||||
"when": "editorLangId == markdown"
|
||||
"when": "editorLangId == markdown && !notebookEditorFocused"
|
||||
},
|
||||
{
|
||||
"command": "markdown.showPreviewToSide",
|
||||
"key": "ctrl+k v",
|
||||
"mac": "cmd+k v",
|
||||
"when": "editorLangId == markdown"
|
||||
"when": "editorLangId == markdown && !notebookEditorFocused"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
|
||||
@@ -13,9 +13,9 @@ import { isMarkdownFile } from '../util/file';
|
||||
|
||||
|
||||
export interface OpenDocumentLinkArgs {
|
||||
readonly path: string;
|
||||
readonly path: {};
|
||||
readonly fragment: string;
|
||||
readonly fromResource: any;
|
||||
readonly fromResource: {};
|
||||
}
|
||||
|
||||
enum OpenMarkdownLinks {
|
||||
@@ -29,13 +29,22 @@ export class OpenDocumentLinkCommand implements Command {
|
||||
|
||||
public static createCommandUri(
|
||||
fromResource: vscode.Uri,
|
||||
path: string,
|
||||
path: vscode.Uri,
|
||||
fragment: string,
|
||||
): vscode.Uri {
|
||||
const toJson = (uri: vscode.Uri) => {
|
||||
return {
|
||||
scheme: uri.scheme,
|
||||
authority: uri.authority,
|
||||
path: uri.path,
|
||||
fragment: uri.fragment,
|
||||
query: uri.query,
|
||||
};
|
||||
};
|
||||
return vscode.Uri.parse(`command:${OpenDocumentLinkCommand.id}?${encodeURIComponent(JSON.stringify(<OpenDocumentLinkArgs>{
|
||||
path: encodeURIComponent(path),
|
||||
path: toJson(path),
|
||||
fragment,
|
||||
fromResource: encodeURIComponent(fromResource.toString(true)),
|
||||
fromResource: toJson(fromResource),
|
||||
}))}`);
|
||||
}
|
||||
|
||||
@@ -43,26 +52,29 @@ export class OpenDocumentLinkCommand implements Command {
|
||||
private readonly engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
public execute(args: OpenDocumentLinkArgs) {
|
||||
const fromResource = vscode.Uri.parse(decodeURIComponent(args.fromResource));
|
||||
const targetPath = decodeURIComponent(args.path);
|
||||
const column = this.getViewColumn(fromResource);
|
||||
return this.tryOpen(targetPath, args, column).catch(() => {
|
||||
if (targetPath && extname(targetPath) === '') {
|
||||
return this.tryOpen(targetPath + '.md', args, column);
|
||||
}
|
||||
const targetResource = vscode.Uri.file(targetPath);
|
||||
return Promise.resolve(undefined)
|
||||
.then(() => vscode.commands.executeCommand('vscode.open', targetResource, column))
|
||||
.then(() => undefined);
|
||||
});
|
||||
public async execute(args: OpenDocumentLinkArgs) {
|
||||
return OpenDocumentLinkCommand.execute(this.engine, args);
|
||||
}
|
||||
|
||||
private async tryOpen(path: string, args: OpenDocumentLinkArgs, column: vscode.ViewColumn) {
|
||||
const resource = vscode.Uri.file(path);
|
||||
public static async execute(engine: MarkdownEngine, args: OpenDocumentLinkArgs) {
|
||||
const fromResource = vscode.Uri.parse('').with(args.fromResource);
|
||||
const targetResource = vscode.Uri.parse('').with(args.path);
|
||||
const column = this.getViewColumn(fromResource);
|
||||
try {
|
||||
return await this.tryOpen(engine, targetResource, args, column);
|
||||
} catch {
|
||||
if (extname(targetResource.path) === '') {
|
||||
return this.tryOpen(engine, targetResource.with({ path: targetResource.path + '.md' }), args, column);
|
||||
}
|
||||
await vscode.commands.executeCommand('vscode.open', targetResource, column);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private static async tryOpen(engine: MarkdownEngine, resource: vscode.Uri, args: OpenDocumentLinkArgs, column: vscode.ViewColumn) {
|
||||
if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) {
|
||||
if (!path || vscode.window.activeTextEditor.document.uri.fsPath === resource.fsPath) {
|
||||
return this.tryRevealLine(vscode.window.activeTextEditor, args.fragment);
|
||||
if (vscode.window.activeTextEditor.document.uri.fsPath === resource.fsPath) {
|
||||
return this.tryRevealLine(engine, vscode.window.activeTextEditor, args.fragment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +85,10 @@ export class OpenDocumentLinkCommand implements Command {
|
||||
|
||||
return vscode.workspace.openTextDocument(resource)
|
||||
.then(document => vscode.window.showTextDocument(document, column))
|
||||
.then(editor => this.tryRevealLine(editor, args.fragment));
|
||||
.then(editor => this.tryRevealLine(engine, editor, args.fragment));
|
||||
}
|
||||
|
||||
private getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
|
||||
private static getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
|
||||
const config = vscode.workspace.getConfiguration('markdown', resource);
|
||||
const openLinks = config.get<OpenMarkdownLinks>('links.openLocation', OpenMarkdownLinks.currentGroup);
|
||||
switch (openLinks) {
|
||||
@@ -88,18 +100,22 @@ export class OpenDocumentLinkCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
private async tryRevealLine(editor: vscode.TextEditor, fragment?: string) {
|
||||
private static async tryRevealLine(engine: MarkdownEngine, editor: vscode.TextEditor, fragment?: string) {
|
||||
if (editor && fragment) {
|
||||
const toc = new TableOfContentsProvider(this.engine, editor.document);
|
||||
const toc = new TableOfContentsProvider(engine, editor.document);
|
||||
const entry = await toc.lookup(fragment);
|
||||
if (entry) {
|
||||
return editor.revealRange(new vscode.Range(entry.line, 0, entry.line, 0), vscode.TextEditorRevealType.AtTop);
|
||||
const lineStart = new vscode.Range(entry.line, 0, entry.line, 0);
|
||||
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
|
||||
return editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
|
||||
}
|
||||
const lineNumberFragment = fragment.match(/^L(\d+)$/i);
|
||||
if (lineNumberFragment) {
|
||||
const line = +lineNumberFragment[1] - 1;
|
||||
if (!isNaN(line)) {
|
||||
return editor.revealRange(new vscode.Range(line, 0, line, 0), vscode.TextEditorRevealType.AtTop);
|
||||
const lineStart = new vscode.Range(line, 0, line, 0);
|
||||
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
|
||||
return editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ import MarkdownWorkspaceSymbolProvider from './features/workspaceSymbolProvider'
|
||||
import { Logger } from './logger';
|
||||
import { MarkdownEngine } from './markdownEngine';
|
||||
import { getMarkdownExtensionContributions } from './markdownExtensions';
|
||||
import { ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector, ContentSecurityPolicyArbiter } from './security';
|
||||
import { loadDefaultTelemetryReporter, TelemetryReporter } from './telemetryReporter';
|
||||
import { ContentSecurityPolicyArbiter, ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './security';
|
||||
import { githubSlugifier } from './slugify';
|
||||
import { loadDefaultTelemetryReporter, TelemetryReporter } from './telemetryReporter';
|
||||
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
@@ -33,7 +33,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
const contentProvider = new MarkdownContentProvider(engine, context, cspArbiter, contributions, logger);
|
||||
const symbolProvider = new MDDocumentSymbolProvider(engine);
|
||||
const previewManager = new MarkdownPreviewManager(contentProvider, logger, contributions);
|
||||
const previewManager = new MarkdownPreviewManager(contentProvider, logger, contributions, engine);
|
||||
context.subscriptions.push(previewManager);
|
||||
|
||||
context.subscriptions.push(registerMarkdownLanguageFeatures(symbolProvider, engine));
|
||||
|
||||
@@ -14,8 +14,7 @@ const localize = nls.loadMessageBundle();
|
||||
function parseLink(
|
||||
document: vscode.TextDocument,
|
||||
link: string,
|
||||
base: string
|
||||
): { uri: vscode.Uri, tooltip?: string } {
|
||||
): { uri: vscode.Uri, tooltip?: string } | undefined {
|
||||
const externalSchemeUri = getUriForLinkWithKnownExternalScheme(link);
|
||||
if (externalSchemeUri) {
|
||||
// Normalize VS Code links to target currently running version
|
||||
@@ -29,24 +28,43 @@ function parseLink(
|
||||
// Use a fake scheme to avoid parse warnings
|
||||
const tempUri = vscode.Uri.parse(`vscode-resource:${link}`);
|
||||
|
||||
let resourcePath = tempUri.path;
|
||||
if (!tempUri.path && document.uri.scheme === 'file') {
|
||||
resourcePath = document.uri.path;
|
||||
let resourceUri: vscode.Uri | undefined;
|
||||
if (!tempUri.path) {
|
||||
resourceUri = document.uri;
|
||||
} else if (tempUri.path[0] === '/') {
|
||||
const root = vscode.workspace.getWorkspaceFolder(document.uri);
|
||||
const root = getWorkspaceFolder(document);
|
||||
if (root) {
|
||||
resourcePath = path.join(root.uri.fsPath, tempUri.path);
|
||||
resourceUri = vscode.Uri.joinPath(root, tempUri.path);
|
||||
}
|
||||
} else {
|
||||
resourcePath = base ? path.join(base, tempUri.path) : tempUri.path;
|
||||
if (document.uri.scheme === Schemes.untitled) {
|
||||
const root = getWorkspaceFolder(document);
|
||||
if (root) {
|
||||
resourceUri = vscode.Uri.joinPath(root, tempUri.path);
|
||||
}
|
||||
} else {
|
||||
const base = document.uri.with({ path: path.dirname(document.uri.fsPath) });
|
||||
resourceUri = vscode.Uri.joinPath(base, tempUri.path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!resourceUri) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
resourceUri = resourceUri.with({ fragment: tempUri.fragment });
|
||||
|
||||
return {
|
||||
uri: OpenDocumentLinkCommand.createCommandUri(document.uri, resourcePath, tempUri.fragment),
|
||||
uri: OpenDocumentLinkCommand.createCommandUri(document.uri, resourceUri, tempUri.fragment),
|
||||
tooltip: localize('documentLink.tooltip', 'Follow link')
|
||||
};
|
||||
}
|
||||
|
||||
function getWorkspaceFolder(document: vscode.TextDocument) {
|
||||
return vscode.workspace.getWorkspaceFolder(document.uri)?.uri
|
||||
|| vscode.workspace.workspaceFolders?.[0]?.uri;
|
||||
}
|
||||
|
||||
function matchAll(
|
||||
pattern: RegExp,
|
||||
text: string
|
||||
@@ -62,7 +80,6 @@ function matchAll(
|
||||
|
||||
function extractDocumentLink(
|
||||
document: vscode.TextDocument,
|
||||
base: string,
|
||||
pre: number,
|
||||
link: string,
|
||||
matchIndex: number | undefined
|
||||
@@ -71,11 +88,14 @@ function extractDocumentLink(
|
||||
const linkStart = document.positionAt(offset);
|
||||
const linkEnd = document.positionAt(offset + link.length);
|
||||
try {
|
||||
const { uri, tooltip } = parseLink(document, link, base);
|
||||
const linkData = parseLink(document, link);
|
||||
if (!linkData) {
|
||||
return undefined;
|
||||
}
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
new vscode.Range(linkStart, linkEnd),
|
||||
uri);
|
||||
documentLink.tooltip = tooltip;
|
||||
linkData.uri);
|
||||
documentLink.tooltip = linkData.tooltip;
|
||||
return documentLink;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
@@ -91,27 +111,25 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
document: vscode.TextDocument,
|
||||
_token: vscode.CancellationToken
|
||||
): vscode.DocumentLink[] {
|
||||
const base = document.uri.scheme === 'file' ? path.dirname(document.uri.fsPath) : '';
|
||||
const text = document.getText();
|
||||
|
||||
return [
|
||||
...this.providerInlineLinks(text, document, base),
|
||||
...this.provideReferenceLinks(text, document, base)
|
||||
...this.providerInlineLinks(text, document),
|
||||
...this.provideReferenceLinks(text, document)
|
||||
];
|
||||
}
|
||||
|
||||
private providerInlineLinks(
|
||||
text: string,
|
||||
document: vscode.TextDocument,
|
||||
base: string
|
||||
): vscode.DocumentLink[] {
|
||||
const results: vscode.DocumentLink[] = [];
|
||||
for (const match of matchAll(this.linkPattern, text)) {
|
||||
const matchImage = match[4] && extractDocumentLink(document, base, match[3].length + 1, match[4], match.index);
|
||||
const matchImage = match[4] && extractDocumentLink(document, match[3].length + 1, match[4], match.index);
|
||||
if (matchImage) {
|
||||
results.push(matchImage);
|
||||
}
|
||||
const matchLink = extractDocumentLink(document, base, match[1].length, match[5], match.index);
|
||||
const matchLink = extractDocumentLink(document, match[1].length, match[5], match.index);
|
||||
if (matchLink) {
|
||||
results.push(matchLink);
|
||||
}
|
||||
@@ -122,7 +140,6 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
private provideReferenceLinks(
|
||||
text: string,
|
||||
document: vscode.TextDocument,
|
||||
base: string
|
||||
): vscode.DocumentLink[] {
|
||||
const results: vscode.DocumentLink[] = [];
|
||||
|
||||
@@ -159,8 +176,10 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
|
||||
for (const definition of definitions.values()) {
|
||||
try {
|
||||
const { uri } = parseLink(document, definition.link, base);
|
||||
results.push(new vscode.DocumentLink(definition.linkRange, uri));
|
||||
const linkData = parseLink(document, definition.link);
|
||||
if (linkData) {
|
||||
results.push(new vscode.DocumentLink(definition.linkRange, linkData.uri));
|
||||
}
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import { Logger } from '../logger';
|
||||
import { MarkdownContentProvider } from './previewContentProvider';
|
||||
import { Disposable } from '../util/dispose';
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { OpenDocumentLinkCommand, resolveLinkToMarkdownFile } from '../commands/openDocumentLink';
|
||||
import { Logger } from '../logger';
|
||||
import { MarkdownContributionProvider } from '../markdownExtensions';
|
||||
import { Disposable } from '../util/dispose';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
import { normalizeResource, WebviewResourceProvider } from '../util/resources';
|
||||
import { getVisibleLine, TopmostLineMonitor } from '../util/topmostLineMonitor';
|
||||
import { MarkdownPreviewConfigurationManager } from './previewConfig';
|
||||
import { MarkdownContributionProvider } from '../markdownExtensions';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
import { resolveLinkToMarkdownFile } from '../commands/openDocumentLink';
|
||||
import { WebviewResourceProvider, normalizeResource } from '../util/resources';
|
||||
import { MarkdownContentProvider } from './previewContentProvider';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
interface WebviewMessage {
|
||||
@@ -123,6 +123,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
resource: vscode.Uri,
|
||||
startingScroll: StartingScrollLocation | undefined,
|
||||
private readonly delegate: MarkdownPreviewDelegate,
|
||||
private readonly engine: MarkdownEngine,
|
||||
private readonly _contentProvider: MarkdownContentProvider,
|
||||
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
private readonly _logger: Logger,
|
||||
@@ -407,7 +408,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
vscode.commands.executeCommand('_markdown.openDocumentLink', { path: hrefPath, fragment, fromResource: this.resource });
|
||||
OpenDocumentLinkCommand.execute(this.engine, { path: hrefPath, fragment, fromResource: this.resource.toJSON() });
|
||||
}
|
||||
|
||||
//#region WebviewResourceProvider
|
||||
@@ -452,8 +453,9 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown
|
||||
previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
logger: Logger,
|
||||
contributionProvider: MarkdownContributionProvider,
|
||||
engine: MarkdownEngine,
|
||||
): StaticMarkdownPreview {
|
||||
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, logger, contributionProvider);
|
||||
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, logger, contributionProvider, engine);
|
||||
}
|
||||
|
||||
private readonly preview: MarkdownPreview;
|
||||
@@ -465,13 +467,14 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown
|
||||
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
logger: Logger,
|
||||
contributionProvider: MarkdownContributionProvider,
|
||||
engine: MarkdownEngine,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, undefined, {
|
||||
getAdditionalState: () => { return {}; },
|
||||
openPreviewLinkToMarkdownFile: () => { /* todo */ }
|
||||
}, contentProvider, _previewConfigurations, logger, contributionProvider));
|
||||
}, engine, contentProvider, _previewConfigurations, logger, contributionProvider));
|
||||
|
||||
this._register(this._webviewPanel.onDidDispose(() => {
|
||||
this.dispose();
|
||||
@@ -548,9 +551,10 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
logger: Logger,
|
||||
topmostLineMonitor: TopmostLineMonitor,
|
||||
contributionProvider: MarkdownContributionProvider,
|
||||
engine: MarkdownEngine,
|
||||
): DynamicMarkdownPreview {
|
||||
return new DynamicMarkdownPreview(webview, input,
|
||||
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider);
|
||||
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, engine);
|
||||
}
|
||||
|
||||
public static create(
|
||||
@@ -560,7 +564,8 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
logger: Logger,
|
||||
topmostLineMonitor: TopmostLineMonitor,
|
||||
contributionProvider: MarkdownContributionProvider
|
||||
contributionProvider: MarkdownContributionProvider,
|
||||
engine: MarkdownEngine,
|
||||
): DynamicMarkdownPreview {
|
||||
const webview = vscode.window.createWebviewPanel(
|
||||
DynamicMarkdownPreview.viewType,
|
||||
@@ -568,7 +573,7 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
previewColumn, { enableFindWidget: true, });
|
||||
|
||||
return new DynamicMarkdownPreview(webview, input,
|
||||
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider);
|
||||
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, engine);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
@@ -579,6 +584,7 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
private readonly _logger: Logger,
|
||||
private readonly _topmostLineMonitor: TopmostLineMonitor,
|
||||
private readonly _contributionProvider: MarkdownContributionProvider,
|
||||
private readonly _engine: MarkdownEngine,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -612,7 +618,12 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
}));
|
||||
|
||||
this._register(vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
if (editor && isMarkdownFile(editor.document) && !this._locked && !this._preview.isPreviewOf(editor.document.uri)) {
|
||||
// Only allow previewing normal text editors which have a viewColumn: See #101514
|
||||
if (typeof editor?.viewColumn === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMarkdownFile(editor.document) && !this._locked && !this._preview.isPreviewOf(editor.document.uri)) {
|
||||
const line = getVisibleLine(editor);
|
||||
this.update(editor.document.uri, line ? new StartingScrollLine(line) : undefined);
|
||||
}
|
||||
@@ -724,6 +735,7 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
|
||||
this.update(link, fragment ? new StartingScrollFragment(fragment) : undefined);
|
||||
}
|
||||
},
|
||||
this._engine,
|
||||
this._contentProvider,
|
||||
this._previewConfigurations,
|
||||
this._logger,
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Logger } from '../logger';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { MarkdownContributionProvider } from '../markdownExtensions';
|
||||
import { disposeAll, Disposable } from '../util/dispose';
|
||||
import { Disposable, disposeAll } from '../util/dispose';
|
||||
import { TopmostLineMonitor } from '../util/topmostLineMonitor';
|
||||
import { DynamicMarkdownPreview, StaticMarkdownPreview, ManagedMarkdownPreview } from './preview';
|
||||
import { DynamicMarkdownPreview, ManagedMarkdownPreview, StaticMarkdownPreview } from './preview';
|
||||
import { MarkdownPreviewConfigurationManager } from './previewConfig';
|
||||
import { MarkdownContentProvider } from './previewContentProvider';
|
||||
|
||||
@@ -68,7 +69,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
public constructor(
|
||||
private readonly _contentProvider: MarkdownContentProvider,
|
||||
private readonly _logger: Logger,
|
||||
private readonly _contributions: MarkdownContributionProvider
|
||||
private readonly _contributions: MarkdownContributionProvider,
|
||||
private readonly _engine: MarkdownEngine,
|
||||
) {
|
||||
super();
|
||||
this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this));
|
||||
@@ -145,7 +147,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
this._previewConfigurations,
|
||||
this._logger,
|
||||
this._topmostLineMonitor,
|
||||
this._contributions);
|
||||
this._contributions,
|
||||
this._engine);
|
||||
|
||||
this.registerDynamicPreview(preview);
|
||||
}
|
||||
@@ -160,7 +163,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
this._contentProvider,
|
||||
this._previewConfigurations,
|
||||
this._logger,
|
||||
this._contributions);
|
||||
this._contributions,
|
||||
this._engine);
|
||||
this.registerStaticPreview(preview);
|
||||
}
|
||||
|
||||
@@ -179,7 +183,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
this._previewConfigurations,
|
||||
this._logger,
|
||||
this._topmostLineMonitor,
|
||||
this._contributions);
|
||||
this._contributions,
|
||||
this._engine);
|
||||
|
||||
this.setPreviewActiveContext(true);
|
||||
this._activePreview = preview;
|
||||
|
||||
@@ -70,7 +70,7 @@ export namespace MarkdownContributions {
|
||||
|
||||
const previewStyles = getContributedStyles(contributions, extension);
|
||||
const previewScripts = getContributedScripts(contributions, extension);
|
||||
const previewResourceRoots = previewStyles.length || previewScripts.length ? [vscode.Uri.file(extension.extensionPath)] : [];
|
||||
const previewResourceRoots = previewStyles.length || previewScripts.length ? [extension.extensionUri] : [];
|
||||
const markdownItPlugins = getContributedMarkdownItPlugins(contributions, extension);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { joinLines } from './util';
|
||||
|
||||
const testFileA = workspaceFile('a.md');
|
||||
|
||||
function workspaceFile(...segments: string[]) {
|
||||
return vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, ...segments);
|
||||
}
|
||||
|
||||
async function getLinksForFile(file: vscode.Uri): Promise<vscode.DocumentLink[]> {
|
||||
return (await vscode.commands.executeCommand<vscode.DocumentLink[]>('vscode.executeLinkProvider', file))!;
|
||||
}
|
||||
|
||||
suite('Markdown Document links', () => {
|
||||
|
||||
teardown(async () => {
|
||||
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
});
|
||||
|
||||
test('Should navigate to markdown file', async () => {
|
||||
await withFileContents(testFileA, '[b](b.md)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('b.md'));
|
||||
});
|
||||
|
||||
test('Should navigate to markdown file with leading ./', async () => {
|
||||
await withFileContents(testFileA, '[b](./b.md)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('b.md'));
|
||||
});
|
||||
|
||||
test('Should navigate to markdown file with leading /', async () => {
|
||||
await withFileContents(testFileA, '[b](./b.md)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('b.md'));
|
||||
});
|
||||
|
||||
test('Should navigate to markdown file without file extension', async () => {
|
||||
await withFileContents(testFileA, '[b](b)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('b.md'));
|
||||
});
|
||||
|
||||
test('Should navigate to markdown file in directory', async () => {
|
||||
await withFileContents(testFileA, '[b](sub/c)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('sub', 'c.md'));
|
||||
});
|
||||
|
||||
test('Should navigate to fragment by title in file', async () => {
|
||||
await withFileContents(testFileA, '[b](sub/c#second)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('sub', 'c.md'));
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 1);
|
||||
});
|
||||
|
||||
test('Should navigate to fragment by line', async () => {
|
||||
await withFileContents(testFileA, '[b](sub/c#L2)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('sub', 'c.md'));
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 1);
|
||||
});
|
||||
|
||||
test('Should navigate to fragment within current file', async () => {
|
||||
await withFileContents(testFileA, joinLines(
|
||||
'[](a#header)',
|
||||
'[](#header)',
|
||||
'# Header'));
|
||||
|
||||
const links = await getLinksForFile(testFileA);
|
||||
{
|
||||
await executeLink(links[0]);
|
||||
assertActiveDocumentUri(workspaceFile('a.md'));
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 2);
|
||||
}
|
||||
{
|
||||
await executeLink(links[1]);
|
||||
assertActiveDocumentUri(workspaceFile('a.md'));
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 2);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should navigate to fragment within current untitled file', async () => {
|
||||
const testFile = workspaceFile('x.md').with({ scheme: 'untitled' });
|
||||
await withFileContents(testFile, joinLines(
|
||||
'[](#second)',
|
||||
'# Second'));
|
||||
|
||||
const [link] = await getLinksForFile(testFile);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(testFile);
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function assertActiveDocumentUri(expectedUri: vscode.Uri) {
|
||||
assert.strictEqual(
|
||||
vscode.window.activeTextEditor!.document.uri.fsPath,
|
||||
expectedUri.fsPath
|
||||
);
|
||||
}
|
||||
|
||||
async function withFileContents(file: vscode.Uri, contents: string): Promise<void> {
|
||||
const document = await vscode.workspace.openTextDocument(file);
|
||||
const editor = await vscode.window.showTextDocument(document);
|
||||
await editor.edit(edit => {
|
||||
edit.replace(new vscode.Range(0, 0, 1000, 0), contents);
|
||||
});
|
||||
}
|
||||
|
||||
async function executeLink(link: vscode.DocumentLink) {
|
||||
const args = JSON.parse(decodeURIComponent(link.target!.query));
|
||||
await vscode.commands.executeCommand(link.target!.path, args);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import LinkProvider from '../features/documentLinkProvider';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
const testFile = vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, 'x.md');
|
||||
|
||||
const noopToken = new class implements vscode.CancellationToken {
|
||||
private _onCancellationRequestedEmitter = new vscode.EventEmitter<void>();
|
||||
@@ -20,7 +20,7 @@ const noopToken = new class implements vscode.CancellationToken {
|
||||
};
|
||||
|
||||
function getLinksForFile(fileContents: string) {
|
||||
const doc = new InMemoryDocument(testFileName, fileContents);
|
||||
const doc = new InMemoryDocument(testFile, fileContents);
|
||||
const provider = new LinkProvider();
|
||||
return provider.provideDocumentLinks(doc, noopToken);
|
||||
}
|
||||
@@ -118,24 +118,24 @@ suite('markdown.DocumentLinkProvider', () => {
|
||||
const links = getLinksForFile('[](https://example.com)');
|
||||
assert.strictEqual(links.length, 2);
|
||||
const [link1, link2] = links;
|
||||
assertRangeEqual(link1.range, new vscode.Range(0,13,0,22));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0,25,0,44));
|
||||
assertRangeEqual(link1.range, new vscode.Range(0, 13, 0, 22));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0, 25, 0, 44));
|
||||
}
|
||||
{
|
||||
const links = getLinksForFile('[]( https://whitespace.com )');
|
||||
assert.strictEqual(links.length, 2);
|
||||
const [link1, link2] = links;
|
||||
assertRangeEqual(link1.range, new vscode.Range(0,7,0,21));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0,26,0,48));
|
||||
assertRangeEqual(link1.range, new vscode.Range(0, 7, 0, 21));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0, 26, 0, 48));
|
||||
}
|
||||
{
|
||||
const links = getLinksForFile('[](file1.txt) text [](file2.txt)');
|
||||
assert.strictEqual(links.length, 4);
|
||||
const [link1, link2, link3, link4] = links;
|
||||
assertRangeEqual(link1.range, new vscode.Range(0,6,0,14));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0,17,0,26));
|
||||
assertRangeEqual(link3.range, new vscode.Range(0,39,0,47));
|
||||
assertRangeEqual(link4.range, new vscode.Range(0,50,0,59));
|
||||
assertRangeEqual(link1.range, new vscode.Range(0, 6, 0, 14));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0, 17, 0, 26));
|
||||
assertRangeEqual(link3.range, new vscode.Range(0, 39, 0, 47));
|
||||
assertRangeEqual(link4.range, new vscode.Range(0, 50, 0, 59));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
DO NOT DELETE, USED BY INTEGRATION TESTS
|
||||
8
extensions/markdown-language-features/src/test/util.ts
Normal file
8
extensions/markdown-language-features/src/test/util.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as os from 'os';
|
||||
|
||||
export const joinLines = (...args: string[]) =>
|
||||
args.join(os.platform() === 'win32' ? '\r\n' : '\n');
|
||||
@@ -9,6 +9,7 @@ export const Schemes = {
|
||||
http: 'http:',
|
||||
https: 'https:',
|
||||
file: 'file:',
|
||||
untitled: 'untitled',
|
||||
mailto: 'mailto:',
|
||||
data: 'data:',
|
||||
vscode: 'vscode:',
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[b](b)
|
||||
[b](b.md)
|
||||
[b](./b.md)
|
||||
[b](/b.md)
|
||||
@@ -0,0 +1,3 @@
|
||||
# b
|
||||
|
||||
[](./a)
|
||||
@@ -0,0 +1,6 @@
|
||||
# First
|
||||
# Second
|
||||
|
||||
[b](/b.md)
|
||||
[b](../b.md)
|
||||
[b](./../b.md)
|
||||
@@ -26,7 +26,7 @@ interface IToken {
|
||||
refreshToken: string;
|
||||
|
||||
account: {
|
||||
displayName: string;
|
||||
label: string;
|
||||
id: string;
|
||||
};
|
||||
scope: string;
|
||||
@@ -48,7 +48,8 @@ interface IStoredSession {
|
||||
refreshToken: string;
|
||||
scope: string; // Scopes are alphabetized and joined with a space
|
||||
account: {
|
||||
displayName: string,
|
||||
label?: string;
|
||||
displayName?: string,
|
||||
id: string
|
||||
}
|
||||
}
|
||||
@@ -101,7 +102,7 @@ export class AzureActiveDirectoryService {
|
||||
accessToken: undefined,
|
||||
refreshToken: session.refreshToken,
|
||||
account: {
|
||||
displayName: session.account.displayName,
|
||||
label: session.account.label ?? session.account.displayName!,
|
||||
id: session.account.id
|
||||
},
|
||||
scope: session.scope,
|
||||
@@ -437,7 +438,7 @@ export class AzureActiveDirectoryService {
|
||||
scope,
|
||||
sessionId: existingId || `${claims.tid}/${(claims.oid || (claims.altsecid || '' + claims.ipd || ''))}/${uuid()}`,
|
||||
account: {
|
||||
displayName: claims.email || claims.unique_name || 'user@example.com',
|
||||
label: claims.email || claims.unique_name || 'user@example.com',
|
||||
id: `${claims.tid}/${(claims.oid || (claims.altsecid || '' + claims.ipd || ''))}`
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,27 +19,42 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
context.subscriptions.push(vscode.authentication.registerAuthenticationProvider({
|
||||
id: 'microsoft',
|
||||
displayName: 'Microsoft',
|
||||
label: 'Microsoft',
|
||||
supportsMultipleAccounts: true,
|
||||
onDidChangeSessions: onDidChangeSessions.event,
|
||||
getSessions: () => Promise.resolve(loginService.sessions),
|
||||
login: async (scopes: string[]) => {
|
||||
try {
|
||||
/* __GDPR__
|
||||
"login" : { }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('login');
|
||||
|
||||
const session = await loginService.login(scopes.sort().join(' '));
|
||||
onDidChangeSessions.fire({ added: [session.id], removed: [], changed: [] });
|
||||
return session;
|
||||
} catch (e) {
|
||||
/* __GDPR__
|
||||
"loginFailed" : { }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('loginFailed');
|
||||
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
logout: async (id: string) => {
|
||||
try {
|
||||
/* __GDPR__
|
||||
"logout" : { }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('logout');
|
||||
|
||||
await loginService.logout(id);
|
||||
onDidChangeSessions.fire({ added: [], removed: [id], changed: [] });
|
||||
} catch (e) {
|
||||
/* __GDPR__
|
||||
"logoutFailed" : { }
|
||||
*/
|
||||
telemetryReporter.sendTelemetryEvent('logoutFailed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { JupyterNotebookManager } from '../../jupyter/jupyterNotebookManager';
|
||||
import { JupyterSessionManager, JupyterSession } from '../../jupyter/jupyterSessionManager';
|
||||
import { LocalJupyterServerManager } from '../../jupyter/jupyterServerManager';
|
||||
import { TestKernel } from '../common';
|
||||
import { sleep } from '../common/testUtils';
|
||||
|
||||
describe('Completion Item Provider', function () {
|
||||
let completionItemProvider: NotebookCompletionItemProvider;
|
||||
@@ -64,7 +65,7 @@ describe('Completion Item Provider', function () {
|
||||
it('should not provide items when session does not exist in notebook provider', async () => {
|
||||
let notebook = await notebookUtils.newNotebook();
|
||||
await notebookUtils.addCell('code');
|
||||
let document = vscode.workspace.textDocuments.find(d => d.uri.path === notebook.document.cells[0].uri.path);
|
||||
let document = await tryFindTextDocument(notebook);
|
||||
should(document).not.equal(undefined, 'Could not find text document that matched cell uri path');
|
||||
|
||||
let completionItems = await completionItemProvider.provideCompletionItems(document, undefined, undefined, undefined);
|
||||
@@ -76,7 +77,7 @@ describe('Completion Item Provider', function () {
|
||||
|
||||
let notebook = await notebookUtils.newNotebook();
|
||||
await notebookUtils.addCell('code');
|
||||
let document = vscode.workspace.textDocuments.find(d => d.uri.path === notebook.document.cells[0].uri.path);
|
||||
let document = await tryFindTextDocument(notebook);
|
||||
|
||||
let completionItems = await completionItemProvider.provideCompletionItems(document, undefined, undefined, undefined);
|
||||
should(completionItems).deepEqual([]);
|
||||
@@ -87,7 +88,7 @@ describe('Completion Item Provider', function () {
|
||||
|
||||
let notebook = await notebookUtils.newNotebook();
|
||||
await notebookUtils.addCell('code');
|
||||
let document = vscode.workspace.textDocuments.find(d => d.uri.path === notebook.document.cells[0].uri.path);
|
||||
let document = await tryFindTextDocument(notebook);
|
||||
|
||||
mockJupyterSession.setup(s => s.path).returns(() => document.uri.path);
|
||||
|
||||
@@ -103,7 +104,7 @@ describe('Completion Item Provider', function () {
|
||||
|
||||
let notebook = await notebookUtils.newNotebook();
|
||||
await notebookUtils.addCell('code');
|
||||
let document = vscode.workspace.textDocuments.find(d => d.uri.path === notebook.document.cells[0].uri.path);
|
||||
let document = await tryFindTextDocument(notebook);
|
||||
|
||||
mockJupyterSession.setup(s => s.path).returns(() => document.uri.path);
|
||||
|
||||
@@ -151,7 +152,7 @@ describe('Completion Item Provider', function () {
|
||||
source: 'sample text'
|
||||
});
|
||||
});
|
||||
let document = vscode.workspace.textDocuments.find(d => d.uri.path === notebook.document.cells[0].uri.path);
|
||||
let document = await tryFindTextDocument(notebook);
|
||||
|
||||
let completionItems = await completionItemProvider.provideCompletionItems(document, new vscode.Position(1, 1), token, undefined);
|
||||
should(completionItems).deepEqual([]);
|
||||
@@ -192,7 +193,18 @@ describe('Completion Item Provider', function () {
|
||||
} else {
|
||||
await notebookUtils.addCell('code');
|
||||
}
|
||||
let document = await tryFindTextDocument(notebook);
|
||||
return document;
|
||||
}
|
||||
|
||||
async function tryFindTextDocument(notebook: azdata.nb.NotebookEditor): Promise<vscode.TextDocument> {
|
||||
let document = vscode.workspace.textDocuments.find(d => d.uri.path === notebook.document.cells[0].uri.path);
|
||||
let triesRemaining = 10;
|
||||
while (!document && triesRemaining > 0) {
|
||||
await sleep(500);
|
||||
document = vscode.workspace.textDocuments.find(d => d.uri.path === notebook.document.cells[0].uri.path);
|
||||
triesRemaining--;
|
||||
}
|
||||
return document;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"description": "Dependencies shared by all extensions",
|
||||
"dependencies": {
|
||||
"typescript": "3.9.5"
|
||||
"typescript": "3.9.6"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node ./postinstall"
|
||||
|
||||
@@ -122,5 +122,11 @@
|
||||
"foreground": "#CBEDCB",
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"semanticTokenColors": {
|
||||
"newOperator": "#FFFFFF",
|
||||
"stringLiteral": "#ce9178",
|
||||
"customLiteral": "#DCDCAA",
|
||||
"numberLiteral": "#b5cea8",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,9 @@
|
||||
"statusBarItem.remoteBackground": "#16825D",
|
||||
"sideBarSectionHeader.background": "#0000",
|
||||
"sideBarSectionHeader.border": "#61616130",
|
||||
"notebook.cellFocusBackground": "#c8ddf150",
|
||||
"notebook.focusedCellBackground": "#c8ddf150",
|
||||
"notebook.cellBorderColor": "#dae3e9",
|
||||
"notebook.outputContainerBackgroundColor": "#c8ddf150",
|
||||
"notebook.focusedCellShadow": "#00315040"
|
||||
"notebook.outputContainerBackgroundColor": "#c8ddf150"
|
||||
},
|
||||
"semanticHighlighting": true
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ Or discuss debug adapters on Gitter:
|
||||
|
||||
You can now 'step through' the 'readme.md' file, set and hit breakpoints, and run into exceptions (if the word exception appears in a line).
|
||||
|
||||

|
||||

|
||||
|
||||
## Build and Run
|
||||
|
||||
@@ -302,3 +302,9 @@ export function getImageFile(): Uint8Array {
|
||||
const data = atob(`/9j/4AAQSkZJRgABAQAASABIAAD/2wCEAA4ODg4ODhcODhchFxcXIS0hISEhLTktLS0tLTlFOTk5OTk5RUVFRUVFRUVSUlJSUlJgYGBgYGxsbGxsbGxsbGwBERISGxkbLxkZL3FMP0xxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcf/AABEIAFYAZAMBIgACEQEDEQH/xAB1AAACAwEBAQAAAAAAAAAAAAAABAMFBgIBBxAAAgIBAwMCBQQCAwAAAAAAAQIAAxEEBSESMUFRcRMiIzJhFIGRoQbBQlKxAQEBAQEAAAAAAAAAAAAAAAABAgADEQEBAQADAQEAAAAAAAAAAAAAARESITECQf/aAAwDAQACEQMRAD8A2LEZkLc/bKxbdYEHWoyfEze56zXpqRTTYUyPHiVrY2TVZyMzhFZMg8iYE6jcVXAusY98KMnj2lhRu+4aLoGuTNTYPV5APnyDNyPFp6EY3EsO3kxnVVLZVg8z2tw9YsXkGQpcbGIbxHQzep0vw8Jgc8n28CJJRY30lBwzf1iaa2ku/HmMV01VW/k/6hh0abTDTafpPcTytmckEewjeosAqJEj0yDo6yO/rFLzoGME5nIAXtGSM9uwnjLn8zFECw7QneITMWouR7gj9/Ep94061bjXa32WDGfzOGuCXKy9/wDc0FlFe5aX4OpHJHBHcSfT4w246bWJar6MsCwKnp9DOF0r6XRiu5snvg9hNK217vQeih0tXwzcED895R7voNfWoN9gOT2QH/2T3mHrda3Y+p9ppZuSV/qR0j6r+5ju2oun2ypOwCAASGikISzdySf5lxLsAdRPpIqw91xC/wDHvGbAAh88RnSVCjT9b8E/MYsguerTqWuYKo8k4ESTcttsPSmoQ+zCZPWPbvWqsvLE0IxCL4wPP7xEW7TXeKsvaGABOMdLef2ky7ejevX0tBWy5Qhh6jmS9IIxPm6XazbW69K56M/aeRibnSaqyytWtGCfE0+tazDhrHpCdixT5EJSWD1BPkcjsYxpN21FWEcdu0dG3hl8rIX0YqUgDqkSrq/0+6oyfOOZT7hqxqLMKMk8ARfS0fqGatAR04yCY+u3OpLt38e0rQl0tzsFrc8rxj0lqqDHMzujIXUMGPI4mjS1MTCvG8gRLddYE2811n5nHTJ9RaAsztzZ1AZhlX9fBi0VWgWzbSqahfpWfa/iSnatMuqOpVgVPIHGMzc6erS3aQVOoZSMFTK19i2pTwGA9Axx/E58b+K2M8lP6/Urp6BkA5Y+OPE112nrIFeOw8RMajQ7dWU0iAH8TyrVG0mw8EypMFuk7K9TS5RGJHiEYsuUtmEWO1KO2RGDRSVJzj1MiQhOQIx8QEYK5hGpUUJVc1lTgcDjEe1FPxqGQHBZSMiQqa8/Z38xgOoHB/aIfJNVZrdFqirsVbsfzLXT7+UQLYmcDHBlh/k+g+KP1dOCV+4efcTNbdtGq3CxQiMKyeX7CGqxqtDuK7lYK2BXnAz3JMuNZoPpDAyV5zHNt2bRbcA1S/Pjljyf7jerWxx0V4wQeZgynxrUXoUnIif629GJY595cptr1N9XJYjOfEi1G3LYMLgH1m04qxelrAtnj/qZYIvUPpMcHwYtTT8FzVaMN6+sslqVF6gcQ1sRivPccwjS314+bGYRBnqzws6FhUfL7CQ8gdI7+TDIHHgcSVGBYRznMXfUL2J5ngPUOYCpfM2tiq1tnUpVRnMe0DGtAKyQIw+mU4GJCKmrPy+I6V0lxYYIzxOCtdjZyVIMRqtPsYx8RT37+sdRhsFlHzcyC0J0kmcfqFX5cxC7VAk4OPUQtM+UVtYf7vH8iKP8SnKg5U9xHQwsGV7jxF9QnWACMEcgwlUjT4ZUE+YRRLGRehwciEpLRMAAT6SALlIQkF4kl7HEIQLwuQfac9RPeEJi5H3TruvvmEJo1QOcgGQuvVg+sITM8rDKeDHVItXkQhKgqM6esnJEIQlJf//Z`);
|
||||
return Uint8Array.from([...data].map(x => x.charCodeAt(0)));
|
||||
}
|
||||
|
||||
// encoded from 'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя'
|
||||
export const windows1251File = Uint8Array.from([192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]);
|
||||
|
||||
// encoded from '中国abc'
|
||||
export const gbkFile = Uint8Array.from([214, 208, 185, 250, 97, 98, 99]);
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
Uri,
|
||||
workspace,
|
||||
} from 'vscode';
|
||||
import { largeTSFile, getImageFile, debuggableFile } from './exampleFiles';
|
||||
import { largeTSFile, getImageFile, debuggableFile, windows1251File, gbkFile } from './exampleFiles';
|
||||
|
||||
export class File implements FileStat {
|
||||
|
||||
@@ -123,6 +123,19 @@ export class MemFS implements FileSystemProvider, FileSearchProvider, TextSearch
|
||||
this.writeFile(Uri.parse(`memfs:/sample-folder/xyz/UPPER.txt`), textEncoder.encode('UPPER'), { create: true, overwrite: true });
|
||||
this.writeFile(Uri.parse(`memfs:/sample-folder/xyz/upper.txt`), textEncoder.encode('upper'), { create: true, overwrite: true });
|
||||
this.writeFile(Uri.parse(`memfs:/sample-folder/xyz/def/foo.md`), textEncoder.encode('*MemFS*'), { create: true, overwrite: true });
|
||||
|
||||
// some files in different encodings
|
||||
this.createDirectory(Uri.parse(`memfs:/sample-folder/encodings/`));
|
||||
this.writeFile(
|
||||
Uri.parse(`memfs:/sample-folder/encodings/windows1251.txt`),
|
||||
windows1251File,
|
||||
{ create: true, overwrite: true }
|
||||
);
|
||||
this.writeFile(
|
||||
Uri.parse(`memfs:/sample-folder/encodings/gbk.txt`),
|
||||
gbkFile,
|
||||
{ create: true, overwrite: true }
|
||||
);
|
||||
}
|
||||
|
||||
root = new Directory(Uri.parse('memfs:/'), '');
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
".dita",
|
||||
".ditamap",
|
||||
".dtd",
|
||||
".ent",
|
||||
".mod",
|
||||
".ent",
|
||||
".mod",
|
||||
".dtml",
|
||||
".fsproj",
|
||||
".fxml",
|
||||
|
||||
@@ -76,10 +76,10 @@ rimraf@^3.0.2:
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
typescript@3.9.5:
|
||||
version "3.9.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36"
|
||||
integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==
|
||||
typescript@3.9.6:
|
||||
version "3.9.6"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a"
|
||||
integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
|
||||
Reference in New Issue
Block a user