Merge branch 'ads-main-vscode-2020-07-15T23-51-12' into main

This commit is contained in:
cssuh
2020-07-17 15:00:28 -04:00
558 changed files with 15180 additions and 8232 deletions

View File

@@ -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'
}
});

View File

@@ -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',

View File

@@ -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"

View File

@@ -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);
}
});
}

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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());

View 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[];
}

View 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;
}

View File

@@ -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');

View File

@@ -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))));

View File

@@ -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"

View File

@@ -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;

View File

@@ -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> {

View File

@@ -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"

View File

@@ -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);
}
}

View File

@@ -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 });
}
}

View File

@@ -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');
}

View File

@@ -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
);
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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;
}
}
}

View File

@@ -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)

View 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);
}
}
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;

View 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;
}
}

View File

@@ -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',

View 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);
}
};
}

View File

@@ -48,14 +48,14 @@
"JSON with Comments"
],
"extensions": [
".hintrc",
".babelrc",
".jsonc",
".eslintrc",
".eslintrc.json",
".jsfmtrc",
".jshintrc",
".swcrc"
".swcrc",
".hintrc",
".babelrc"
],
"configuration": "./language-configuration.json"
}

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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": {

View File

@@ -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);
}
}
}

View File

@@ -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));

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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('[![alt text](image.jpg)](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('[![a]( whitespace.jpg )]( 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('[![a](img1.jpg)](file1.txt) text [![a](img2.jpg)](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));
}
});
});

View File

@@ -1 +0,0 @@
DO NOT DELETE, USED BY INTEGRATION TESTS

View 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');

View File

@@ -9,6 +9,7 @@ export const Schemes = {
http: 'http:',
https: 'https:',
file: 'file:',
untitled: 'untitled',
mailto: 'mailto:',
data: 'data:',
vscode: 'vscode:',

View File

@@ -0,0 +1,4 @@
[b](b)
[b](b.md)
[b](./b.md)
[b](/b.md)

View File

@@ -0,0 +1,3 @@
# b
[](./a)

View File

@@ -0,0 +1,6 @@
# First
# Second
[b](/b.md)
[b](../b.md)
[b](./../b.md)

View File

@@ -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 || ''))}`
}
};

View File

@@ -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');
}
}

View File

@@ -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;
}
});

View File

@@ -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"

View File

@@ -122,5 +122,11 @@
"foreground": "#CBEDCB",
}
}
]
],
"semanticTokenColors": {
"newOperator": "#FFFFFF",
"stringLiteral": "#ce9178",
"customLiteral": "#DCDCAA",
"numberLiteral": "#b5cea8",
}
}

View File

@@ -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
}

View File

@@ -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).
![Mock Debug](images/mock-debug.gif)
![Mock Debug](file.jpg)
## 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]);

View File

@@ -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:/'), '');

View File

@@ -23,8 +23,8 @@
".dita",
".ditamap",
".dtd",
".ent",
".mod",
".ent",
".mod",
".dtml",
".fsproj",
".fxml",

View File

@@ -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"