Merge vscode source through release 1.79.2 (#23482)

* log when an editor action doesn't run because of enablement

* notebooks create/dispose editors. this means controllers must be created eagerly (😢) and that notebooks need a custom way of plugging comparision keys for session. works unless creating another session for the same cell of a duplicated editor

* Set offSide to sql lang configuration to true (#183461)

* Fixes #181764 (#183550)

* fix typo

* Always scroll down and focus the input (#183557)

* Fixes #180386 (#183561)

* cli: ensure ordering of rpc server messages (#183558)

* cli: ensure ordering of rpc server messages

Sending lots of messages to a stream would block them around the async
tokio mutex, which is "fair" so doesn't preserve ordering. Instead, use
the write_loop approach I introduced to the server_multiplexer for the
same reason some time ago.

* fix clippy

* update for May endgame

* testing: allow invalidateTestResults to take an array (#183569)

* Document `ShareProvider` API proposal (#183568)

* Document `ShareProvider` API proposal

* Remove mention of VS Code from JSDoc

* Add support for rendering svg and md in welcome message (#183580)

* Remove toggle setting more eagerly (#183584)

* rm message abt macOS

* Change text (#183589)

* Change text

* Accidentally changed the wrong file

* cli: improve output for code tunnel status (#183571)

* testing: allow invalidateTestResults to take an array

* cli: improve output for code tunnel status

Fixes #183570

* [json/css/html] update services (#183595)

* Add experimental setting to enable this dialog

* Fix exporting chat model to JSON before it is initialized (#183597)

* minimum scrolling to reveal the next cell on shift+enter (#183600)

do minimum scrolling to reveal the next cell on Execute cell and select next

* Fixing Jupyter notebook issue 13263 (#183527)

fix for the issue, still need to understand why there is strange focusing

* Tweak proposed API JSDoc (#183590)

* Tweak proposed API JSDoc

* workbench -> workspace

* fix ? operator

* Use active editor and show progress when sharing (#183603)

Use active editor and show progress

* use scroll setting variable correctly

* Schedule welcome widget to show once between typing. (#183606)

* Schedule dialog to show once between typing

* Don't re-render if already displayed once

* Add F10 keybinding for debugger step, even on Web. (#183510)

Fixes #181792.
Previously, for Web the keyboard shortcut was Alt-F10, because it was
believed that F10 could not be bound on browsers. This turned out to be
incorrect, so we make the shortcut consistent (F10) with desktop VSCode
which is also what many other debuggers use.
We keep Alt-F10 on web as a secondary keybinding to keep the experience
some web users may have gotten used to by now.

* Also pass process.env

* Restore missing chat clear commands (#183651)

* chore: update electron@22.5.4 (#183716)

* Show remote indicator in web when remoteAuthority is set (#183728)

* feat: .vuerc as json file (#153017)

Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>

* Delete --compatibility=1.63 code from the server (#183738)

* Copy vscode.dev link to tunnel generates an invalid link when an untitled workspace is open (#183739)

* Recent devcontainer display string corrupted on Get Started page (#183740)

* Improve "next codeblock" navigation (#183744)

* Improve "next codeblock" navigation
Operate on the current focused response, or the last one, and scroll to the selected item

* Normalize command title

* Git - run git status if similarityThreshold changes (#183762)

* fix aria-label issue in kb editor

fixes A11y_GradeB_VSCode_Keyboard shortcut reads words together - Blind: Arrow key navigation to row Find the binding keys and  "when" cell data are read together resulting in a word " CTRL + FeditorFocus  instead of CTRL + F editorFocus" #182490

* Status - fix compact padding (#183768)

* Remove angle brackets from VB brackets (#183782)

Fixes #183359

* Update language config schema with more details about brackets. (#183779)

* fix comment (#183812)

* Support for `Notebook` CodeAction Kind (#183457)

* nb kind support -- wip

* allow notebook codeactions around single cell edit check

* move notebook code action type out of editor

---------

Co-authored-by: rebornix <penn.lv@gmail.com>

* cli: fix connection default being applied (#183827)

* cli: bump to openssl 1.1.1u (#183828)

* Implement "delete" action for chat history (#183609)

* Use desired file name when generating new md pasted file paths (#183861)

Fixes #183851

* Default to filename for markdown new file if empty (#183864)

Fixes #183848

* Fix small typo (#183865)

Fixes #183819

* Noop when moving a symbol into the file it is already in (#183866)

Fixes #183793

* Adjust codeAction validation to account for notebook kind (#183859)

* Make JS/TS `go to configuration` commands work on non-`file:` file systems (#183688)

Make `go to project` commands work on non-`file:` file systems

Fixes #183685

* Can't do regex search after opening notebook (#183884)

Fixes #183858

* Default to current dir for `move to file` select (#183875)

Fixes #183870

`showOpenDialog` seems to ignore `defaultUri` if the file doesn't exist

* Use `<...>` style markdown links when needed (#183876)

Fixes #183849

* Remove check for context keys

* Update xterm package

* Enable updating a chat model without triggering incremental typing (#183894)

* Enable chat "move" commands on empty sessions (#183895)

* Enable chat "move" commands on empty sessions
and also imported sessions

* Fix command name

* Fix some chat keybindings on windows (#183896)

* "Revert File" on inactive editors are ignored (fix #177557) (#183903)

* Empty reason while switching profile (fix #183775) (#183904)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4278 (#183910)

* fix https://github.com/microsoft/vscode/issues/183770 (#183914)

* code --status displays a lot of errors before actual status output (fix #183787) (#183915)

* joh/icy manatee (#183917)

* Use idle value for widget of interactive editor controller

https://github.com/microsoft/vscode/issues/183820

* also make preview editors idle values

https://github.com/microsoft/vscode/issues/183820

* Fix #183777 (#183929)

* Fix #182309 (#183925)

* Tree checkbox item -> items (#183931)

Fixes #183826

* Fixes #183909 (#183940)

* Fix #183837 (#183943)

fix #183837

* Git - fix #183941 (#183944)

* Update xterm.css

Fixes #181242

* chore: add @ulugbekna and @aiday-mar to my-endgame notebook (#183946)

* Revert "When snippet mode is active, make `Tab` not accept suggestion but advance placeholder"

This reverts commit 50a80cdb61511343996ff1d41d0b676c3d329f48.

* revert not focusing completion list when quick suggest happens during snippet

* change `snippetsPreventQuickSuggestions` default to false

* Fix #181446 (#183956)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4298 (#183957)

* fix: remove extraneous incorrect context keys (#183959)

These were actually getting added in getTestItemContextOverlay, and the test ID was using the extended ID which extensions do not know about.

Fixes #183612

* Fixes https://github.com/microsoft/monaco-editor/issues/3920 (#183960)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4324 (#183961)

* fix #183030

* fix #180826 (#183962)

* make message more generic for interactive editor help

* .

* fix #183968

* Keep codeblock toolbar visible when focused

* Fix when clause on "Run in terminal" command

* add important info to help menu

* fix #183970

* Set `isRefactoring` for all TS refactoring edits (#183982)

* consolidate

* Disable move to file in TS versions < 5.2 (#183992)

There are still a few key bugs with refactoring. We will  ship this as a preview for TS 5.2+ instead of for 5.1

* Polish query accepting (#183995)

We shouldn't send the same request to Copilot if the query hasn't changed. So if the query is the same, we short circut.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4286

Also, when we open in chat, we should use the last accepted query, not what's in the input box.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4280

* Allow widget to have focus (#184000)

So that selecting non-code text works.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4294

* Fix microsoft/vscode-internalbacklog#4257. Mitigate zindex for zone widgets. (#184001)

* Change welcome dialog contribution to Eventually

* Misc fixes

* Workspace folder picker entry descriptions are suboptimal for some filesystems (fix #183418) (#184018)

* cli - ignore std error unless verbose (#183787) (#184031)

* joh/inquisitive meerkat (#184034)

* only stash sessions that are none empty

https://github.com/microsoft/vscode-internalbacklog/issues/4281

* only unstash a session once - unless new exchanges are made,

https://github.com/microsoft/vscode-internalbacklog/issues/4281

* account for all exchange types

* Improve declared components (#184039)

* make sure to read setting (#184040)

d'oh, related to https://github.com/microsoft/vscode/issues/173387#issuecomment-1571696644

* [html] update service (#184049)

[html] update service. FIxes #181176

* reset context keys on reset/hide (#184042)

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4330

* use `Lazy`, not `IdleValue` for the IE widget held by the eager controller (#184048)

https://github.com/microsoft/vscode/issues/183820

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4333 (#184067)

* use undo-loop instead of undo-edit when discarding chat session (#184063)

* use undo-loop instead of undo-edit when discarding chat session

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4118

* fix tests, wait for correct state

* Add logging to node download (#184070)

Add logging to node download. For #182951

* re-enable default zone widget revealing when showing (#184072)

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4332, also fixes https://github.com/microsoft/vscode-internalbacklog/issues/3784

* fix #178202

* Allow APIs in stable (#184062)

* Fix microsoft/vscode-internalbacklog#4206. Override List view whitespace css for monaco editor (#184087)

* Fix JSDoc grammatical error (#184090)

* Pick up TS 5.1.3 (#184091)

Fixes #182931

* Misc fixes

* update distro (#184097)

* chore: update electron@22.5.5 (#184116)

* Extension host veto is registered multiple times on restart (fix #183778) (#184127)

Extension host veto is registered multiple times on restart (#183778)

* Do not auto start the local web worker extension host (#184137)

* Allow embedders to intercept trustedTypes.createPolicy calls (#184136)

Allow embedders to intercept trustedTypes.createPolicy calls (#184100)

* fix: reading from console output for --status on windows and linux (#184138)

fix: reading from console output for --status on windows and linux (#184118)

* Misc fixes

* code --status displays a lot of errors before actual status output (fix #183787) (#184200)

fix 183787

* (cherry-pick to 1.79 from main) Handle galleryExtension failure in featuredExtensionService (#184205)

Handle galleryExtension failure in featuredExtensionService (#184198)

Handle galleryExtension failure

* Fix #184183. Multiple output height updates are skipped. (#184188)

* Post merge init fixes

* Misc build issues

* disable toggle inline diff of `alt` down

https://github.com/microsoft/vscode-internalbacklog/issues/4342

* Take into account already activated extensions when computing running locations (#184303)

Take into account already activated extensions when computing running locations (fixes #184180)

* Avoid `extensionService.getExtension` and use `ActivationKind.Immediate` to allow that URI handling works while resolving (#184310)

Avoid `extensionService.getExtension` and use `ActivationKind.Immediate` to allow that URI handling works while resolving (fixes #182217)

* WIP

* rm fish auto injection

* More breaks

* Fix Port Attributes constructor (#184412)

* WIP

* WIP

* Allow extensions to get at the exports of other extensions during resolving (#184487)

Allow extensions to get at the exports of other extensions during resolving (fixes #184472)

* do not auto finish session when inline chat widgets have focus

re https://github.com/microsoft/vscode-internalbacklog/issues/4354

* fix compile errors caused by new base method

* WIP

* WIP

* WIP

* WIP

* Build errors

* unc - fix path traversal bypass

* Bump version

* cherry-pick prod changes from main

* Disable sandbox

* Build break from merge

* bump version

* Merge pull request #184739 from max06/max06/issue184659

Restore ShellIntegration for fish (#184659)

* Git - only add --find-renames if the value is not the default one (#185053)

Git - only add --find-renames if the value is not the default one (#184992)

* Cherry-pick: Revert changes to render featured extensions when available (#184747)

Revert changes to render featured extensions when available.  (#184573)

* Lower timeouts for experimentation and gallery service

* Revert changes to render extensions when available

* Add audio cues

* fix: disable app sandbox when --no-sandbox is present (#184913)

* fix: disable app sandbox when --no-sandbox is present (#184897)

* fix: loading minimist in packaged builds

* Runtime errors

* UNC allow list checks cannot be disabled in extension host (fix #184989) (#185085)

* UNC allow list checks cannot be disabled in extension host (#184989)

* Update src/vs/base/node/unc.js

Co-authored-by: Robo <hop2deep@gmail.com>

---------

Co-authored-by: Robo <hop2deep@gmail.com>

* Add notebook extension

* Fix mangling issues

* Fix mangling issues

* npm install

* npm install

* Issues blocking bundle

* Fix build folder compile errors

* Fix windows bundle build

* Linting fixes

* Fix sqllint issues

* Update yarn.lock files

* Fix unit tests

* Fix a couple breaks from test fixes

* Bump distro

* redo the checkbox style

* Update linux build container dockerfile

* Bump build image tag

* Bump native watch dog package

* Bump node-pty

* Bump distro

* Fix documnetation error

* Update distro

* redo the button styles

* Update datasource TS

* Add missing yarn.lock files

* Windows setup fix

* Turn off extension unit tests while investigating

* color box style

* Remove appx

* Turn off test log upload

* update dropdownlist style

* fix universal app build error (#23488)

* Skip flaky bufferContext vscode test

---------

Co-authored-by: Johannes <johannes.rieken@gmail.com>
Co-authored-by: Henning Dieterichs <hdieterichs@microsoft.com>
Co-authored-by: Julien Richard <jairbubbles@hotmail.com>
Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
Co-authored-by: Megan Rogge <merogge@microsoft.com>
Co-authored-by: meganrogge <megan.rogge@microsoft.com>
Co-authored-by: Rob Lourens <roblourens@gmail.com>
Co-authored-by: Connor Peet <connor@peet.io>
Co-authored-by: Joyce Er <joyce.er@microsoft.com>
Co-authored-by: Bhavya U <bhavyau@microsoft.com>
Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com>
Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>
Co-authored-by: Aaron Munger <aamunger@microsoft.com>
Co-authored-by: Aiday Marlen Kyzy <amarlenkyzy@microsoft.com>
Co-authored-by: rebornix <penn.lv@gmail.com>
Co-authored-by: Ole <oler@google.com>
Co-authored-by: Jean Pierre <jeanp413@hotmail.com>
Co-authored-by: Robo <hop2deep@gmail.com>
Co-authored-by: Yash Singh <saiansh2525@gmail.com>
Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com>
Co-authored-by: Ulugbek Abdullaev <ulugbekna@gmail.com>
Co-authored-by: Alex Ross <alros@microsoft.com>
Co-authored-by: Michael Lively <milively@microsoft.com>
Co-authored-by: Matt Bierner <matb@microsoft.com>
Co-authored-by: Andrea Mah <31675041+andreamah@users.noreply.github.com>
Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
Co-authored-by: Sandeep Somavarapu <sasomava@microsoft.com>
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Co-authored-by: Tyler James Leonhardt <me@tylerleonhardt.com>
Co-authored-by: Alexandru Dima <alexdima@microsoft.com>
Co-authored-by: Joao Moreno <Joao.Moreno@microsoft.com>
Co-authored-by: Alan Ren <alanren@microsoft.com>
This commit is contained in:
Karl Burtram
2023-06-27 15:26:51 -07:00
committed by GitHub
parent 7975fda6dd
commit 01e66ab3e6
4335 changed files with 252586 additions and 164604 deletions

View File

@@ -5,18 +5,42 @@
import * as vscode from 'vscode';
import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import { IMdParser } from './markdownEngine';
import { IMdParser } from '../markdownEngine';
import * as proto from './protocol';
import { looksLikeMarkdownPath, markdownFileExtensions } from './util/file';
import { IMdWorkspace } from './workspace';
import { looksLikeMarkdownPath, markdownFileExtensions } from '../util/file';
import { VsCodeMdWorkspace } from './workspace';
import { FileWatcherManager } from './fileWatchingManager';
import { IDisposable } from '../util/dispose';
const localize = nls.loadMessageBundle();
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
export class MdLanguageClient implements IDisposable {
export async function startClient(factory: LanguageClientConstructor, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
constructor(
private readonly _client: BaseLanguageClient,
private readonly _workspace: VsCodeMdWorkspace,
) { }
dispose(): void {
this._client.stop();
this._workspace.dispose();
}
resolveLinkTarget(linkText: string, uri: vscode.Uri): Promise<proto.ResolvedDocumentLinkTarget> {
return this._client.sendRequest(proto.resolveLinkTarget, { linkText, uri: uri.toString() });
}
getEditForFileRenames(files: ReadonlyArray<{ oldUri: string; newUri: string }>, token: vscode.CancellationToken) {
return this._client.sendRequest(proto.getEditForFileRenames, files, token);
}
getReferencesToFileInWorkspace(resource: vscode.Uri, token: vscode.CancellationToken) {
return this._client.sendRequest(proto.getReferencesToFileInWorkspace, { uri: resource.toString() }, token);
}
}
export async function startClient(factory: LanguageClientConstructor, parser: IMdParser): Promise<MdLanguageClient> {
const mdFileGlob = `**/*.{${markdownFileExtensions.join(',')}}`;
@@ -28,19 +52,18 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
},
initializationOptions: {
markdownFileExtensions,
i10lLocation: vscode.l10n.uri?.toJSON(),
},
diagnosticPullOptions: {
onChange: true,
onSave: true,
onTabs: true,
match(_documentSelector, resource) {
return looksLikeMarkdownPath(resource);
},
},
};
const client = factory('markdown', localize('markdownServer.name', 'Markdown Language Server'), clientOptions);
const client = factory('markdown', vscode.l10n.t("Markdown Language Server"), clientOptions);
client.registerProposedFeatures();
@@ -57,6 +80,8 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
});
}
const workspace = new VsCodeMdWorkspace();
client.onRequest(proto.parse, async (e) => {
const uri = vscode.Uri.parse(e.uri);
const doc = await workspace.getOrLoadMarkdownDocument(uri);
@@ -92,23 +117,36 @@ export async function startClient(factory: LanguageClientConstructor, workspace:
return (await vscode.workspace.findFiles(mdFileGlob, '**/node_modules/**')).map(x => x.toString());
});
const watchers = new Map<number, vscode.FileSystemWatcher>();
const watchers = new FileWatcherManager();
client.onRequest(proto.fs_watcher_create, async (params): Promise<void> => {
const id = params.id;
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.parse(params.uri), '*'), params.options.ignoreCreate, params.options.ignoreChange, params.options.ignoreDelete);
watchers.set(id, watcher);
watcher.onDidCreate(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'create' }); });
watcher.onDidChange(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'change' }); });
watcher.onDidDelete(() => { client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind: 'delete' }); });
const uri = vscode.Uri.parse(params.uri);
const sendWatcherChange = (kind: 'create' | 'change' | 'delete') => {
client.sendRequest(proto.fs_watcher_onChange, { id, uri: params.uri, kind });
};
watchers.create(id, uri, params.watchParentDirs, {
create: params.options.ignoreCreate ? undefined : () => sendWatcherChange('create'),
change: params.options.ignoreChange ? undefined : () => sendWatcherChange('change'),
delete: params.options.ignoreDelete ? undefined : () => sendWatcherChange('delete'),
});
});
client.onRequest(proto.fs_watcher_delete, async (params): Promise<void> => {
watchers.get(params.id)?.dispose();
watchers.delete(params.id);
});
vscode.commands.registerCommand('vscodeMarkdownLanguageservice.open', (uri, args) => {
return vscode.commands.executeCommand('vscode.open', uri, args);
});
vscode.commands.registerCommand('vscodeMarkdownLanguageservice.rename', (uri, pos) => {
return vscode.commands.executeCommand('editor.action.rename', [vscode.Uri.from(uri), new vscode.Position(pos.line, pos.character)]);
});
await client.start();
return client;
return new MdLanguageClient(client, workspace);
}

View File

@@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* 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 { Utils } from 'vscode-uri';
import { disposeAll, IDisposable } from '../util/dispose';
import { ResourceMap } from '../util/resourceMap';
import { Schemes } from '../util/schemes';
type DirWatcherEntry = {
readonly uri: vscode.Uri;
readonly listeners: IDisposable[];
};
export class FileWatcherManager {
private readonly _fileWatchers = new Map<number, {
readonly watcher: vscode.FileSystemWatcher;
readonly dirWatchers: DirWatcherEntry[];
}>();
private readonly _dirWatchers = new ResourceMap<{
readonly watcher: vscode.FileSystemWatcher;
refCount: number;
}>();
create(id: number, uri: vscode.Uri, watchParentDirs: boolean, listeners: { create?: () => void; change?: () => void; delete?: () => void }): void {
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(uri, '*'), !listeners.create, !listeners.change, !listeners.delete);
const parentDirWatchers: DirWatcherEntry[] = [];
this._fileWatchers.set(id, { watcher, dirWatchers: parentDirWatchers });
if (listeners.create) { watcher.onDidCreate(listeners.create); }
if (listeners.change) { watcher.onDidChange(listeners.change); }
if (listeners.delete) { watcher.onDidDelete(listeners.delete); }
if (watchParentDirs && uri.scheme !== Schemes.untitled) {
// We need to watch the parent directories too for when these are deleted / created
for (let dirUri = Utils.dirname(uri); dirUri.path.length > 1; dirUri = Utils.dirname(dirUri)) {
const dirWatcher: DirWatcherEntry = { uri: dirUri, listeners: [] };
let parentDirWatcher = this._dirWatchers.get(dirUri);
if (!parentDirWatcher) {
const glob = new vscode.RelativePattern(Utils.dirname(dirUri), Utils.basename(dirUri));
const parentWatcher = vscode.workspace.createFileSystemWatcher(glob, !listeners.create, true, !listeners.delete);
parentDirWatcher = { refCount: 0, watcher: parentWatcher };
this._dirWatchers.set(dirUri, parentDirWatcher);
}
parentDirWatcher.refCount++;
if (listeners.create) {
dirWatcher.listeners.push(parentDirWatcher.watcher.onDidCreate(async () => {
// Just because the parent dir was created doesn't mean our file was created
try {
const stat = await vscode.workspace.fs.stat(uri);
if (stat.type === vscode.FileType.File) {
listeners.create!();
}
} catch {
// Noop
}
}));
}
if (listeners.delete) {
// When the parent dir is deleted, consider our file deleted too
// TODO: this fires if the file previously did not exist and then the parent is deleted
dirWatcher.listeners.push(parentDirWatcher.watcher.onDidDelete(listeners.delete));
}
parentDirWatchers.push(dirWatcher);
}
}
}
delete(id: number): void {
const entry = this._fileWatchers.get(id);
if (entry) {
for (const dirWatcher of entry.dirWatchers) {
disposeAll(dirWatcher.listeners);
const dirWatcherEntry = this._dirWatchers.get(dirWatcher.uri);
if (dirWatcherEntry) {
if (--dirWatcherEntry.refCount <= 0) {
dirWatcherEntry.watcher.dispose();
this._dirWatchers.delete(dirWatcher.uri);
}
}
}
entry.watcher.dispose();
}
this._fileWatchers.delete(id);
}
}

View File

@@ -3,6 +3,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function isEmptyOrWhitespace(str: string): boolean {
return /^\s*$/.test(str);
import * as vscode from 'vscode';
import { ITextDocument } from '../types/textDocument';
export class InMemoryDocument implements ITextDocument {
constructor(
public readonly uri: vscode.Uri,
private readonly _contents: string,
public readonly version = 0,
) { }
getText(): string {
return this._contents;
}
}

View File

@@ -3,11 +3,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Token = require('markdown-it/lib/token');
import { RequestType } from 'vscode-languageclient';
import type Token = require('markdown-it/lib/token');
import * as vscode from 'vscode';
import { FileRename, RequestType } from 'vscode-languageclient';
import type * as lsp from 'vscode-languageserver-types';
import type * as md from 'vscode-markdown-languageservice';
export type ResolvedDocumentLinkTarget =
| { readonly kind: 'file'; readonly uri: vscode.Uri; position?: lsp.Position; fragment?: string }
| { readonly kind: 'folder'; readonly uri: vscode.Uri }
| { readonly kind: 'external'; readonly uri: vscode.Uri };
//#region From server
export const parse = new RequestType<{ uri: string }, Token[], any>('markdown/parse');
@@ -15,7 +22,7 @@ export const fs_readFile = new RequestType<{ uri: string }, number[], any>('mark
export const fs_readDirectory = new RequestType<{ uri: string }, [string, { isDirectory: boolean }][], any>('markdown/fs/readDirectory');
export const fs_stat = new RequestType<{ uri: string }, { isDirectory: boolean } | undefined, any>('markdown/fs/stat');
export const fs_watcher_create = new RequestType<{ id: number; uri: string; options: md.FileWatcherOptions }, void, any>('markdown/fs/watcher/create');
export const fs_watcher_create = new RequestType<{ id: number; uri: string; options: md.FileWatcherOptions; watchParentDirs: boolean }, void, any>('markdown/fs/watcher/create');
export const fs_watcher_delete = new RequestType<{ id: number }, void, any>('markdown/fs/watcher/delete');
export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('markdown/findMarkdownFilesInWorkspace');
@@ -23,6 +30,9 @@ export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('
//#region To server
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');
export const getEditForFileRenames = new RequestType<Array<FileRename>, { participatingRenames: readonly FileRename[]; edit: lsp.WorkspaceEdit }, any>('markdown/getEditForFileRenames');
export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
export const resolveLinkTarget = new RequestType<{ linkText: string; uri: string }, ResolvedDocumentLinkTarget, any>('markdown/resolveLinkTarget');
//#endregion

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* 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 { ITextDocument } from '../types/textDocument';
import { Disposable } from '../util/dispose';
import { isMarkdownFile, looksLikeMarkdownPath } from '../util/file';
import { InMemoryDocument } from './inMemoryDocument';
import { ResourceMap } from '../util/resourceMap';
/**
* Provides set of markdown files known to VS Code.
*
* This includes both opened text documents and markdown files in the workspace.
*/
export class VsCodeMdWorkspace extends Disposable {
private _watcher: vscode.FileSystemWatcher | undefined;
private readonly _documentCache = new ResourceMap<ITextDocument>();
private readonly _utf8Decoder = new TextDecoder('utf-8');
constructor() {
super();
this._watcher = this._register(vscode.workspace.createFileSystemWatcher('**/*.md'));
this._register(this._watcher.onDidChange(async resource => {
this._documentCache.delete(resource);
}));
this._register(this._watcher.onDidDelete(resource => {
this._documentCache.delete(resource);
}));
this._register(vscode.workspace.onDidOpenTextDocument(e => {
this._documentCache.delete(e.uri);
}));
this._register(vscode.workspace.onDidCloseTextDocument(e => {
this._documentCache.delete(e.uri);
}));
}
private _isRelevantMarkdownDocument(doc: vscode.TextDocument) {
return isMarkdownFile(doc) && doc.uri.scheme !== 'vscode-bulkeditpreview';
}
public async getOrLoadMarkdownDocument(resource: vscode.Uri): Promise<ITextDocument | undefined> {
const existing = this._documentCache.get(resource);
if (existing) {
return existing;
}
const matchingDocument = vscode.workspace.textDocuments.find((doc) => this._isRelevantMarkdownDocument(doc) && doc.uri.toString() === resource.toString());
if (matchingDocument) {
this._documentCache.set(resource, matchingDocument);
return matchingDocument;
}
if (!looksLikeMarkdownPath(resource)) {
return undefined;
}
try {
const bytes = await vscode.workspace.fs.readFile(resource);
// We assume that markdown is in UTF-8
const text = this._utf8Decoder.decode(bytes);
const doc = new InMemoryDocument(resource, text, 0);
this._documentCache.set(resource, doc);
return doc;
} catch {
return undefined;
}
}
}

View File

@@ -13,28 +13,28 @@ export interface Command {
}
export class CommandManager {
private readonly commands = new Map<string, vscode.Disposable>();
private readonly _commands = new Map<string, vscode.Disposable>();
public dispose() {
for (const registration of this.commands.values()) {
for (const registration of this._commands.values()) {
registration.dispose();
}
this.commands.clear();
this._commands.clear();
}
public register<T extends Command>(command: T): vscode.Disposable {
this.registerCommand(command.id, command.execute, command);
this._registerCommand(command.id, command.execute, command);
return new vscode.Disposable(() => {
this.commands.delete(command.id);
this._commands.delete(command.id);
});
}
// {{SQL CARBON EDIT}}
private registerCommand(id: string, impl: (...args: any[]) => any, thisArg?: any) {
if (this.commands.has(id)) {
private _registerCommand(id: string, impl: (...args: any[]) => any, thisArg?: any) {
if (this._commands.has(id)) {
return;
}
this.commands.set(id, vscode.commands.registerCommand(id, impl, thisArg));
this._commands.set(id, vscode.commands.registerCommand(id, impl, thisArg));
}
}

View File

@@ -3,13 +3,41 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export { MoveCursorToPositionCommand } from './moveCursorToPosition';
export { OpenDocumentLinkCommand } from './openDocumentLink';
export { RefreshPreviewCommand } from './refreshPreview';
export { ReloadPlugins } from './reloadPlugins';
export { RenderDocument } from './renderDocument';
export { ShowLockedPreviewToSideCommand, ShowPreviewCommand, ShowPreviewToSideCommand } from './showPreview';
export { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector';
export { ShowSourceCommand } from './showSource';
export { ToggleLockCommand } from './toggleLock';
import * as vscode from 'vscode';
import { CommandManager } from '../commandManager';
import { MarkdownItEngine } from '../markdownEngine';
import { MarkdownPreviewManager } from '../preview/previewManager';
import { ContentSecurityPolicyArbiter, PreviewSecuritySelector } from '../preview/security';
import { TelemetryReporter } from '../telemetryReporter';
import { InsertLinkFromWorkspace, InsertImageFromWorkspace } from './insertResource';
import { RefreshPreviewCommand } from './refreshPreview';
import { ReloadPlugins } from './reloadPlugins';
import { RenderDocument } from './renderDocument';
import { ShowLockedPreviewToSideCommand, ShowPreviewCommand, ShowPreviewToSideCommand } from './showPreview';
import { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector';
import { ShowSourceCommand } from './showSource';
import { ToggleLockCommand } from './toggleLock';
export function registerMarkdownCommands(
commandManager: CommandManager,
previewManager: MarkdownPreviewManager,
telemetryReporter: TelemetryReporter,
cspArbiter: ContentSecurityPolicyArbiter,
engine: MarkdownItEngine,
): vscode.Disposable {
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
commandManager.register(new ShowPreviewCommand(previewManager, telemetryReporter));
commandManager.register(new ShowPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new ShowSourceCommand(previewManager));
commandManager.register(new RefreshPreviewCommand(previewManager, engine));
commandManager.register(new ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager));
commandManager.register(new ToggleLockCommand(previewManager));
commandManager.register(new RenderDocument(engine));
commandManager.register(new ReloadPlugins(previewManager, engine));
commandManager.register(new InsertLinkFromWorkspace());
commandManager.register(new InsertImageFromWorkspace());
return commandManager;
}

View File

@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* 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 { Utils } from 'vscode-uri';
import { Command } from '../commandManager';
import { createUriListSnippet, mediaFileExtensions } from '../languageFeatures/copyFiles/shared';
import { coalesce } from '../util/arrays';
import { getParentDocumentUri } from '../util/document';
import { Schemes } from '../util/schemes';
export class InsertLinkFromWorkspace implements Command {
public readonly id = 'markdown.editor.insertLinkFromWorkspace';
public async execute(resources?: vscode.Uri[]) {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}
resources ??= await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: true,
openLabel: vscode.l10n.t("Insert link"),
title: vscode.l10n.t("Insert link"),
defaultUri: getDefaultUri(activeEditor.document),
});
return insertLink(activeEditor, resources ?? [], false);
}
}
export class InsertImageFromWorkspace implements Command {
public readonly id = 'markdown.editor.insertImageFromWorkspace';
public async execute(resources?: vscode.Uri[]) {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}
resources ??= await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: true,
filters: {
[vscode.l10n.t("Media")]: Array.from(mediaFileExtensions.keys())
},
openLabel: vscode.l10n.t("Insert image"),
title: vscode.l10n.t("Insert image"),
defaultUri: getDefaultUri(activeEditor.document),
});
return insertLink(activeEditor, resources ?? [], true);
}
}
function getDefaultUri(document: vscode.TextDocument) {
const docUri = getParentDocumentUri(document);
if (docUri.scheme === Schemes.untitled) {
return vscode.workspace.workspaceFolders?.[0]?.uri;
}
return Utils.dirname(docUri);
}
async function insertLink(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsImage: boolean): Promise<void> {
if (!selectedFiles.length) {
return;
}
const edit = createInsertLinkEdit(activeEditor, selectedFiles, insertAsImage);
await vscode.workspace.applyEdit(edit);
}
function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsMedia: boolean) {
const snippetEdits = coalesce(activeEditor.selections.map((selection, i): vscode.SnippetTextEdit | undefined => {
const selectionText = activeEditor.document.getText(selection);
const snippet = createUriListSnippet(activeEditor.document, selectedFiles, {
insertAsMedia,
placeholderText: selectionText,
placeholderStartIndex: (i + 1) * selectedFiles.length,
separator: insertAsMedia ? '\n' : ' ',
});
return snippet ? new vscode.SnippetTextEdit(selection, snippet.snippet) : undefined;
}));
const edit = new vscode.WorkspaceEdit();
edit.set(activeEditor.document.uri, snippetEdits);
return edit;
}

View File

@@ -1,21 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { Command } from '../commandManager';
export class MoveCursorToPositionCommand implements Command {
public readonly id = '_markdown.moveCursorToPosition';
public execute(line: number, character: number) {
if (!vscode.window.activeTextEditor) {
return;
}
const position = new vscode.Position(line, character);
const selection = new vscode.Selection(position, position);
vscode.window.activeTextEditor.revealRange(selection);
vscode.window.activeTextEditor.selection = selection;
}
}

View File

@@ -1,67 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { Command } from '../commandManager';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { openDocumentLink } from '../util/openDocumentLink';
import { Schemes } from '../util/schemes';
type UriComponents = {
readonly scheme?: string;
readonly path: string;
readonly fragment?: string;
readonly authority?: string;
readonly query?: string;
};
export interface OpenDocumentLinkArgs {
readonly parts: UriComponents;
readonly fragment: string;
readonly fromResource: UriComponents;
}
export class OpenDocumentLinkCommand implements Command {
private static readonly id = '_markdown.openDocumentLink';
public readonly id = OpenDocumentLinkCommand.id;
public static createCommandUri(
fromResource: vscode.Uri,
path: vscode.Uri,
fragment: string,
): vscode.Uri {
const toJson = (uri: vscode.Uri): UriComponents => {
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>{
parts: toJson(path),
fragment,
fromResource: toJson(fromResource),
}))}`);
}
public constructor(
private readonly tocProvider: MdTableOfContentsProvider,
) { }
public async execute(args: OpenDocumentLinkArgs) {
const fromResource = vscode.Uri.parse('').with(args.fromResource);
const targetResource = reviveUri(args.parts).with({ fragment: args.fragment });
return openDocumentLink(this.tocProvider, targetResource, fromResource);
}
}
function reviveUri(parts: any) {
if (parts.scheme === Schemes.file) {
return vscode.Uri.file(parts.path);
}
return vscode.Uri.parse('').with(parts);
}

View File

@@ -11,12 +11,12 @@ export class RefreshPreviewCommand implements Command {
public readonly id = 'markdown.preview.refresh';
public constructor(
private readonly webviewManager: MarkdownPreviewManager,
private readonly engine: MarkdownItEngine
private readonly _webviewManager: MarkdownPreviewManager,
private readonly _engine: MarkdownItEngine
) { }
public execute() {
this.engine.cleanCache();
this.webviewManager.refresh();
this._engine.cleanCache();
this._webviewManager.refresh();
}
}

View File

@@ -11,13 +11,13 @@ export class ReloadPlugins implements Command {
public readonly id = 'markdown.api.reloadPlugins';
public constructor(
private readonly webviewManager: MarkdownPreviewManager,
private readonly engine: MarkdownItEngine,
private readonly _webviewManager: MarkdownPreviewManager,
private readonly _engine: MarkdownItEngine,
) { }
public execute(): void {
this.engine.reloadPlugins();
this.engine.cleanCache();
this.webviewManager.refresh();
this._engine.reloadPlugins();
this._engine.cleanCache();
this._webviewManager.refresh();
}
}

View File

@@ -11,10 +11,10 @@ export class RenderDocument implements Command {
public readonly id = 'markdown.api.render';
public constructor(
private readonly engine: MarkdownItEngine
private readonly _engine: MarkdownItEngine
) { }
public async execute(document: ITextDocument | string): Promise<string> {
return (await (this.engine.render(document))).html;
return (await (this._engine.render(document))).html;
}
}

View File

@@ -54,13 +54,13 @@ export class ShowPreviewCommand implements Command {
public readonly id = 'markdown.showPreview';
public constructor(
private readonly webviewManager: MarkdownPreviewManager,
private readonly telemetryReporter: TelemetryReporter
private readonly _webviewManager: MarkdownPreviewManager,
private readonly _telemetryReporter: TelemetryReporter
) { }
public execute(mainUri?: vscode.Uri, allUris?: vscode.Uri[], previewSettings?: DynamicPreviewSettings) {
for (const uri of Array.isArray(allUris) ? allUris : [mainUri]) {
showPreview(this.webviewManager, this.telemetryReporter, uri, {
showPreview(this._webviewManager, this._telemetryReporter, uri, {
sideBySide: false,
locked: previewSettings && previewSettings.locked
});
@@ -72,12 +72,12 @@ export class ShowPreviewToSideCommand implements Command {
public readonly id = 'markdown.showPreviewToSide';
public constructor(
private readonly webviewManager: MarkdownPreviewManager,
private readonly telemetryReporter: TelemetryReporter
private readonly _webviewManager: MarkdownPreviewManager,
private readonly _telemetryReporter: TelemetryReporter
) { }
public execute(uri?: vscode.Uri, previewSettings?: DynamicPreviewSettings) {
showPreview(this.webviewManager, this.telemetryReporter, uri, {
showPreview(this._webviewManager, this._telemetryReporter, uri, {
sideBySide: true,
locked: previewSettings && previewSettings.locked
});
@@ -89,12 +89,12 @@ export class ShowLockedPreviewToSideCommand implements Command {
public readonly id = 'markdown.showLockedPreviewToSide';
public constructor(
private readonly webviewManager: MarkdownPreviewManager,
private readonly telemetryReporter: TelemetryReporter
private readonly _webviewManager: MarkdownPreviewManager,
private readonly _telemetryReporter: TelemetryReporter
) { }
public execute(uri?: vscode.Uri) {
showPreview(this.webviewManager, this.telemetryReporter, uri, {
showPreview(this._webviewManager, this._telemetryReporter, uri, {
sideBySide: true,
locked: true
});

View File

@@ -13,18 +13,18 @@ export class ShowPreviewSecuritySelectorCommand implements Command {
public readonly id = 'markdown.showPreviewSecuritySelector';
public constructor(
private readonly previewSecuritySelector: PreviewSecuritySelector,
private readonly previewManager: MarkdownPreviewManager
private readonly _previewSecuritySelector: PreviewSecuritySelector,
private readonly _previewManager: MarkdownPreviewManager
) { }
public execute(resource: string | undefined) {
if (this.previewManager.activePreviewResource) {
this.previewSecuritySelector.showSecuritySelectorForResource(this.previewManager.activePreviewResource);
if (this._previewManager.activePreviewResource) {
this._previewSecuritySelector.showSecuritySelectorForResource(this._previewManager.activePreviewResource);
} else if (resource) {
const source = vscode.Uri.parse(resource);
this.previewSecuritySelector.showSecuritySelectorForResource(source.query ? vscode.Uri.parse(source.query) : source);
this._previewSecuritySelector.showSecuritySelectorForResource(source.query ? vscode.Uri.parse(source.query) : source);
} else if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) {
this.previewSecuritySelector.showSecuritySelectorForResource(vscode.window.activeTextEditor.document.uri);
this._previewSecuritySelector.showSecuritySelectorForResource(vscode.window.activeTextEditor.document.uri);
}
}
}

View File

@@ -11,11 +11,11 @@ export class ShowSourceCommand implements Command {
public readonly id = 'markdown.showSource';
public constructor(
private readonly previewManager: MarkdownPreviewManager
private readonly _previewManager: MarkdownPreviewManager
) { }
public execute() {
const { activePreviewResource, activePreviewResourceColumn } = this.previewManager;
const { activePreviewResource, activePreviewResourceColumn } = this._previewManager;
if (activePreviewResource && activePreviewResourceColumn) {
return vscode.workspace.openTextDocument(activePreviewResource).then(document => {
return vscode.window.showTextDocument(document, activePreviewResourceColumn);

View File

@@ -10,10 +10,10 @@ export class ToggleLockCommand implements Command {
public readonly id = 'markdown.preview.toggleLock';
public constructor(
private readonly previewManager: MarkdownPreviewManager
private readonly _previewManager: MarkdownPreviewManager
) { }
public execute() {
this.previewManager.toggleLock();
this._previewManager.toggleLock();
}
}

View File

@@ -4,14 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { BaseLanguageClient, LanguageClient, LanguageClientOptions } from 'vscode-languageclient/browser';
import { startClient } from './client';
import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient/browser';
import { MdLanguageClient, startClient } from './client/client';
import { activateShared } from './extension.shared';
import { VsCodeOutputLogger } from './logging';
import { IMdParser, MarkdownItEngine } from './markdownEngine';
import { getMarkdownExtensionContributions } from './markdownExtensions';
import { githubSlugifier } from './slugify';
import { IMdWorkspace, VsCodeMdWorkspace } from './workspace';
export async function activate(context: vscode.ExtensionContext) {
const contributions = getMarkdownExtensionContributions(context);
@@ -22,21 +21,18 @@ export async function activate(context: vscode.ExtensionContext) {
const engine = new MarkdownItEngine(contributions, githubSlugifier, logger);
const workspace = new VsCodeMdWorkspace();
context.subscriptions.push(workspace);
const client = await startServer(context, workspace, engine);
context.subscriptions.push({
dispose: () => client.stop()
});
activateShared(context, client, workspace, engine, logger, contributions);
const client = await startServer(context, engine);
context.subscriptions.push(client);
activateShared(context, client, engine, logger, contributions);
}
function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
const serverMain = vscode.Uri.joinPath(context.extensionUri, 'server/dist/browser/main.js');
function startServer(context: vscode.ExtensionContext, parser: IMdParser): Promise<MdLanguageClient> {
const serverMain = vscode.Uri.joinPath(context.extensionUri, 'server/dist/browser/workerMain.js');
const worker = new Worker(serverMain.toString());
worker.postMessage({ i10lLocation: vscode.l10n.uri?.toString() ?? '' });
return startClient((id: string, name: string, clientOptions: LanguageClientOptions) => {
return new LanguageClient(id, name, clientOptions, worker);
}, workspace, parser);
}, parser);
}

View File

@@ -4,27 +4,26 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { BaseLanguageClient } from 'vscode-languageclient';
import { MdLanguageClient } from './client/client';
import { CommandManager } from './commandManager';
import * as commands from './commands/index';
import { registerPasteSupport } from './languageFeatures/copyPaste';
import { registerMarkdownCommands } from './commands/index';
import { registerPasteSupport } from './languageFeatures/copyFiles/copyPaste';
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor';
import { registerDropIntoEditorSupport } from './languageFeatures/copyFiles/dropIntoEditor';
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
import { registerUpdateLinksOnRename } from './languageFeatures/linkUpdater';
import { ILogger } from './logging';
import { MarkdownItEngine, MdParsingProvider } from './markdownEngine';
import { MarkdownItEngine } from './markdownEngine';
import { MarkdownContributionProvider } from './markdownExtensions';
import { MdDocumentRenderer } from './preview/documentRenderer';
import { MarkdownPreviewManager } from './preview/previewManager';
import { ContentSecurityPolicyArbiter, ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './preview/security';
import { MdTableOfContentsProvider } from './tableOfContents';
import { loadDefaultTelemetryReporter, TelemetryReporter } from './telemetryReporter';
import { IMdWorkspace } from './workspace';
import { ExtensionContentSecurityPolicyArbiter } from './preview/security';
import { loadDefaultTelemetryReporter } from './telemetryReporter';
import { MdLinkOpener } from './util/openDocumentLink';
export function activateShared(
context: vscode.ExtensionContext,
client: BaseLanguageClient,
workspace: IMdWorkspace,
client: MdLanguageClient,
engine: MarkdownItEngine,
logger: ILogger,
contributions: MarkdownContributionProvider,
@@ -35,16 +34,14 @@ export function activateShared(
const cspArbiter = new ExtensionContentSecurityPolicyArbiter(context.globalState, context.workspaceState);
const commandManager = new CommandManager();
const parser = new MdParsingProvider(engine, workspace);
const tocProvider = new MdTableOfContentsProvider(parser, workspace, logger);
context.subscriptions.push(parser, tocProvider);
const opener = new MdLinkOpener(client);
const contentProvider = new MdDocumentRenderer(engine, context, cspArbiter, contributions, logger);
const previewManager = new MarkdownPreviewManager(contentProvider, workspace, logger, contributions, tocProvider);
const previewManager = new MarkdownPreviewManager(contentProvider, logger, contributions, opener);
context.subscriptions.push(previewManager);
context.subscriptions.push(registerMarkdownLanguageFeatures(client, commandManager));
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine, tocProvider));
context.subscriptions.push(registerMarkdownCommands(commandManager, previewManager, telemetryReporter, cspArbiter, engine));
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => {
previewManager.updateConfiguration();
@@ -52,7 +49,7 @@ export function activateShared(
}
function registerMarkdownLanguageFeatures(
client: BaseLanguageClient,
client: MdLanguageClient,
commandManager: CommandManager,
): vscode.Disposable {
const selector: vscode.DocumentSelector = { language: 'markdown', scheme: '*' };
@@ -62,29 +59,7 @@ function registerMarkdownLanguageFeatures(
registerDropIntoEditorSupport(selector),
registerFindFileReferenceSupport(commandManager, client),
registerPasteSupport(selector),
registerUpdateLinksOnRename(client),
);
}
function registerMarkdownCommands(
commandManager: CommandManager,
previewManager: MarkdownPreviewManager,
telemetryReporter: TelemetryReporter,
cspArbiter: ContentSecurityPolicyArbiter,
engine: MarkdownItEngine,
tocProvider: MdTableOfContentsProvider,
): vscode.Disposable {
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
commandManager.register(new commands.ShowPreviewCommand(previewManager, telemetryReporter));
commandManager.register(new commands.ShowPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new commands.ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new commands.ShowSourceCommand(previewManager));
commandManager.register(new commands.RefreshPreviewCommand(previewManager, engine));
commandManager.register(new commands.MoveCursorToPositionCommand());
commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager));
commandManager.register(new commands.OpenDocumentLinkCommand(tocProvider));
commandManager.register(new commands.ToggleLockCommand(previewManager));
commandManager.register(new commands.RenderDocument(engine));
commandManager.register(new commands.ReloadPlugins(previewManager, engine));
return commandManager;
}

View File

@@ -4,14 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { BaseLanguageClient, LanguageClient, ServerOptions, TransportKind } from 'vscode-languageclient/node';
import { startClient } from './client';
import { LanguageClient, ServerOptions, TransportKind } from 'vscode-languageclient/node';
import { MdLanguageClient, startClient } from './client/client';
import { activateShared } from './extension.shared';
import { VsCodeOutputLogger } from './logging';
import { IMdParser, MarkdownItEngine } from './markdownEngine';
import { getMarkdownExtensionContributions } from './markdownExtensions';
import { githubSlugifier } from './slugify';
import { IMdWorkspace, VsCodeMdWorkspace } from './workspace';
export async function activate(context: vscode.ExtensionContext) {
const contributions = getMarkdownExtensionContributions(context);
@@ -22,20 +21,15 @@ export async function activate(context: vscode.ExtensionContext) {
const engine = new MarkdownItEngine(contributions, githubSlugifier, logger);
const workspace = new VsCodeMdWorkspace();
context.subscriptions.push(workspace);
const client = await startServer(context, workspace, engine);
context.subscriptions.push({
dispose: () => client.stop()
});
activateShared(context, client, workspace, engine, logger, contributions);
const client = await startServer(context, engine);
context.subscriptions.push(client);
activateShared(context, client, engine, logger, contributions);
}
function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace, parser: IMdParser): Promise<BaseLanguageClient> {
function startServer(context: vscode.ExtensionContext, parser: IMdParser): Promise<MdLanguageClient> {
const clientMain = vscode.extensions.getExtension('vscode.markdown-language-features')?.packageJSON?.main || '';
const serverMain = `./server/${clientMain.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/main`;
const serverMain = `./server/${clientMain.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/workerMain`;
const serverModule = context.asAbsolutePath(serverMain);
// The debug options for the server
@@ -47,7 +41,11 @@ function startServer(context: vscode.ExtensionContext, workspace: IMdWorkspace,
run: { module: serverModule, transport: TransportKind.ipc },
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
};
// pass the location of the localization bundle to the server
process.env['VSCODE_L10N_BUNDLE_LOCATION'] = vscode.l10n.uri?.toString() ?? '';
return startClient((id, name, clientOptions) => {
return new LanguageClient(id, name, serverOptions, clientOptions);
}, workspace, parser);
}, parser);
}

View File

@@ -0,0 +1,174 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as picomatch from 'picomatch';
import * as vscode from 'vscode';
import { Utils } from 'vscode-uri';
import { getParentDocumentUri } from '../../util/document';
type OverwriteBehavior = 'overwrite' | 'nameIncrementally';
interface CopyFileConfiguration {
readonly destination: Record<string, string>;
readonly overwriteBehavior: OverwriteBehavior;
}
function getCopyFileConfiguration(document: vscode.TextDocument): CopyFileConfiguration {
const config = vscode.workspace.getConfiguration('markdown', document);
return {
destination: config.get<Record<string, string>>('copyFiles.destination') ?? {},
overwriteBehavior: readOverwriteBehavior(config),
};
}
function readOverwriteBehavior(config: vscode.WorkspaceConfiguration): OverwriteBehavior {
switch (config.get('copyFiles.overwriteBehavior')) {
case 'overwrite': return 'overwrite';
default: return 'nameIncrementally';
}
}
export class NewFilePathGenerator {
private readonly _usedPaths = new Set<string>();
async getNewFilePath(
document: vscode.TextDocument,
file: vscode.DataTransferFile,
token: vscode.CancellationToken,
): Promise<{ readonly uri: vscode.Uri; readonly overwrite: boolean } | undefined> {
const config = getCopyFileConfiguration(document);
const desiredPath = getDesiredNewFilePath(config, document, file);
const root = Utils.dirname(desiredPath);
const ext = Utils.extname(desiredPath);
let baseName = Utils.basename(desiredPath);
baseName = baseName.slice(0, baseName.length - ext.length);
for (let i = 0; ; ++i) {
if (token.isCancellationRequested) {
return undefined;
}
const name = i === 0 ? baseName : `${baseName}-${i}`;
const uri = vscode.Uri.joinPath(root, name + ext);
if (this._wasPathAlreadyUsed(uri)) {
continue;
}
// Try overwriting if it already exists
if (config.overwriteBehavior === 'overwrite') {
this._usedPaths.add(uri.toString());
return { uri, overwrite: true };
}
// Otherwise we need to check the fs to see if it exists
try {
await vscode.workspace.fs.stat(uri);
} catch {
if (!this._wasPathAlreadyUsed(uri)) {
// Does not exist
this._usedPaths.add(uri.toString());
return { uri, overwrite: false };
}
}
}
}
private _wasPathAlreadyUsed(uri: vscode.Uri) {
return this._usedPaths.has(uri.toString());
}
}
function getDesiredNewFilePath(config: CopyFileConfiguration, document: vscode.TextDocument, file: vscode.DataTransferFile): vscode.Uri {
const docUri = getParentDocumentUri(document);
for (const [rawGlob, rawDest] of Object.entries(config.destination)) {
for (const glob of parseGlob(rawGlob)) {
if (picomatch.isMatch(docUri.path, glob)) {
return resolveCopyDestination(docUri, file.name, rawDest, uri => vscode.workspace.getWorkspaceFolder(uri)?.uri);
}
}
}
// Default to next to current file
return vscode.Uri.joinPath(Utils.dirname(docUri), file.name);
}
function parseGlob(rawGlob: string): Iterable<string> {
if (rawGlob.startsWith('/')) {
// Anchor to workspace folders
return (vscode.workspace.workspaceFolders ?? []).map(folder => vscode.Uri.joinPath(folder.uri, rawGlob).path);
}
// Relative path, so implicitly track on ** to match everything
if (!rawGlob.startsWith('**')) {
return ['**/' + rawGlob];
}
return [rawGlob];
}
type GetWorkspaceFolder = (documentUri: vscode.Uri) => vscode.Uri | undefined;
export function resolveCopyDestination(documentUri: vscode.Uri, fileName: string, dest: string, getWorkspaceFolder: GetWorkspaceFolder): vscode.Uri {
const resolvedDest = resolveCopyDestinationSetting(documentUri, fileName, dest, getWorkspaceFolder);
if (resolvedDest.startsWith('/')) {
// Absolute path
return Utils.resolvePath(documentUri, resolvedDest);
}
// Relative to document
const dirName = Utils.dirname(documentUri);
return Utils.resolvePath(dirName, resolvedDest);
}
function resolveCopyDestinationSetting(documentUri: vscode.Uri, fileName: string, dest: string, getWorkspaceFolder: GetWorkspaceFolder): string {
let outDest = dest.trim();
if (!outDest) {
outDest = '${fileName}';
}
// Destination that start with `/` implicitly means go to workspace root
if (outDest.startsWith('/')) {
outDest = '${documentWorkspaceFolder}/' + outDest.slice(1);
}
// Destination that ends with `/` implicitly needs a fileName
if (outDest.endsWith('/')) {
outDest += '${fileName}';
}
const documentDirName = Utils.dirname(documentUri);
const documentBaseName = Utils.basename(documentUri);
const documentExtName = Utils.extname(documentUri);
const workspaceFolder = getWorkspaceFolder(documentUri);
const vars = new Map<string, string>([
['documentDirName', documentDirName.path], // Parent directory path
['documentFileName', documentBaseName], // Full filename: file.md
['documentBaseName', documentBaseName.slice(0, documentBaseName.length - documentExtName.length)], // Just the name: file
['documentExtName', documentExtName.replace('.', '')], // Just the file ext: md
// Workspace
['documentWorkspaceFolder', (workspaceFolder ?? documentDirName).path],
// File
['fileName', fileName],// Full file name
]);
return outDest.replaceAll(/\$\{(\w+)(?:\/([^\}]+?)\/([^\}]+?)\/)?\}/g, (_, name, pattern, replacement) => {
const entry = vars.get(name);
if (!entry) {
return '';
}
if (pattern && replacement) {
return entry.replace(new RegExp(pattern), replacement);
}
return entry;
});
}

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* 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 { Schemes } from '../../util/schemes';
import { createEditForMediaFiles, mediaMimes, tryGetUriListSnippet } from './shared';
class PasteEditProvider implements vscode.DocumentPasteEditProvider {
private readonly _id = 'insertLink';
async provideDocumentPasteEdits(
document: vscode.TextDocument,
_ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken,
): Promise<vscode.DocumentPasteEdit | undefined> {
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.filePaste.enabled', true);
if (!enabled) {
return;
}
const createEdit = await this._getMediaFilesEdit(document, dataTransfer, token);
if (createEdit) {
return createEdit;
}
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
if (!snippet) {
return;
}
const uriEdit = new vscode.DocumentPasteEdit(snippet.snippet, this._id, snippet.label);
uriEdit.priority = this._getPriority(dataTransfer);
return uriEdit;
}
private async _getMediaFilesEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentPasteEdit | undefined> {
if (document.uri.scheme === Schemes.untitled) {
return;
}
const copyFilesIntoWorkspace = vscode.workspace.getConfiguration('markdown', document).get<'mediaFiles' | 'never'>('editor.filePaste.copyIntoWorkspace', 'mediaFiles');
if (copyFilesIntoWorkspace === 'never') {
return;
}
const edit = await createEditForMediaFiles(document, dataTransfer, token);
if (!edit) {
return;
}
const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, this._id, edit.label);
pasteEdit.additionalEdit = edit.additionalEdits;
pasteEdit.priority = this._getPriority(dataTransfer);
return pasteEdit;
}
private _getPriority(dataTransfer: vscode.DataTransfer): number {
if (dataTransfer.get('text/plain')) {
// Deprioritize in favor of normal text content
return -10;
}
return 0;
}
}
export function registerPasteSupport(selector: vscode.DocumentSelector,) {
return vscode.languages.registerDocumentPasteEditProvider(selector, new PasteEditProvider(), {
pasteMimeTypes: [
'text/uri-list',
...mediaMimes,
]
});
}

View File

@@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* 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 { createEditForMediaFiles as createEditForMediaFiles, tryGetUriListSnippet } from './shared';
import { Schemes } from '../../util/schemes';
class MarkdownImageDropProvider implements vscode.DocumentDropEditProvider {
private readonly _id = 'insertLink';
async provideDocumentDropEdits(document: vscode.TextDocument, _position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.drop.enabled', true);
if (!enabled) {
return;
}
const filesEdit = await this._getMediaFilesEdit(document, dataTransfer, token);
if (filesEdit) {
return filesEdit;
}
if (token.isCancellationRequested) {
return;
}
return this._getUriListEdit(document, dataTransfer, token);
}
private async _getUriListEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
if (!snippet) {
return undefined;
}
const edit = new vscode.DocumentDropEdit(snippet.snippet);
edit.id = this._id;
edit.label = snippet.label;
return edit;
}
private async _getMediaFilesEdit(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
if (document.uri.scheme === Schemes.untitled) {
return;
}
const copyIntoWorkspace = vscode.workspace.getConfiguration('markdown', document).get<'mediaFiles' | 'never'>('editor.drop.copyIntoWorkspace', 'mediaFiles');
if (copyIntoWorkspace !== 'mediaFiles') {
return;
}
const filesEdit = await createEditForMediaFiles(document, dataTransfer, token);
if (!filesEdit) {
return;
}
const edit = new vscode.DocumentDropEdit(filesEdit.snippet);
edit.id = this._id;
edit.label = filesEdit.label;
edit.additionalEdit = filesEdit.additionalEdits;
return edit;
}
}
export function registerDropIntoEditorSupport(selector: vscode.DocumentSelector) {
return vscode.languages.registerDocumentDropEditProvider(selector, new MarkdownImageDropProvider(), {
dropMimeTypes: [
'text/uri-list'
]
});
}

View File

@@ -0,0 +1,298 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import * as URI from 'vscode-uri';
import { Schemes } from '../../util/schemes';
import { NewFilePathGenerator } from './copyFiles';
import { coalesce } from '../../util/arrays';
import { getDocumentDir } from '../../util/document';
enum MediaKind {
Image,
Video,
Audio,
}
export const mediaFileExtensions = new Map<string, MediaKind>([
// Images
['bmp', MediaKind.Image],
['gif', MediaKind.Image],
['ico', MediaKind.Image],
['jpe', MediaKind.Image],
['jpeg', MediaKind.Image],
['jpg', MediaKind.Image],
['png', MediaKind.Image],
['psd', MediaKind.Image],
['svg', MediaKind.Image],
['tga', MediaKind.Image],
['tif', MediaKind.Image],
['tiff', MediaKind.Image],
['webp', MediaKind.Image],
// Videos
['ogg', MediaKind.Video],
['mp4', MediaKind.Video],
// Audio Files
['mp3', MediaKind.Audio],
['aac', MediaKind.Audio],
['wav', MediaKind.Audio],
]);
export const mediaMimes = new Set([
'image/bmp',
'image/gif',
'image/jpeg',
'image/png',
'image/webp',
'video/mp4',
'video/ogg',
'audio/mpeg',
'audio/aac',
'audio/x-wav',
]);
export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<{ snippet: vscode.SnippetString; label: string } | undefined> {
const urlList = await dataTransfer.get('text/uri-list')?.asString();
if (!urlList || token.isCancellationRequested) {
return undefined;
}
const uris: vscode.Uri[] = [];
for (const resource of urlList.split(/\r?\n/g)) {
try {
uris.push(vscode.Uri.parse(resource));
} catch {
// noop
}
}
return createUriListSnippet(document, uris);
}
interface UriListSnippetOptions {
readonly placeholderText?: string;
readonly placeholderStartIndex?: number;
/**
* Should the snippet be for an image link or video?
*
* If `undefined`, tries to infer this from the uri.
*/
readonly insertAsMedia?: boolean;
readonly separator?: string;
}
export function createUriListSnippet(
document: vscode.TextDocument,
uris: readonly vscode.Uri[],
options?: UriListSnippetOptions
): { snippet: vscode.SnippetString; label: string } | undefined {
if (!uris.length) {
return;
}
const dir = getDocumentDir(document);
const snippet = new vscode.SnippetString();
let insertedLinkCount = 0;
let insertedImageCount = 0;
let insertedAudioVideoCount = 0;
uris.forEach((uri, i) => {
const mdPath = getMdPath(dir, uri);
const ext = URI.Utils.extname(uri).toLowerCase().replace('.', '');
const insertAsMedia = typeof options?.insertAsMedia === 'undefined' ? mediaFileExtensions.has(ext) : !!options.insertAsMedia;
const insertAsVideo = mediaFileExtensions.get(ext) === MediaKind.Video;
const insertAsAudio = mediaFileExtensions.get(ext) === MediaKind.Audio;
if (insertAsVideo) {
insertedAudioVideoCount++;
snippet.appendText(`<video src="${escapeHtmlAttribute(mdPath)}" controls title="`);
snippet.appendPlaceholder('Title');
snippet.appendText('"></video>');
} else if (insertAsAudio) {
insertedAudioVideoCount++;
snippet.appendText(`<audio src="${escapeHtmlAttribute(mdPath)}" controls title="`);
snippet.appendPlaceholder('Title');
snippet.appendText('"></audio>');
} else {
if (insertAsMedia) {
insertedImageCount++;
} else {
insertedLinkCount++;
}
snippet.appendText(insertAsMedia ? '![' : '[');
const placeholderText = options?.placeholderText ?? (insertAsMedia ? 'Alt text' : 'label');
const placeholderIndex = typeof options?.placeholderStartIndex !== 'undefined' ? options?.placeholderStartIndex + i : undefined;
snippet.appendPlaceholder(placeholderText, placeholderIndex);
snippet.appendText(`](${escapeMarkdownLinkPath(mdPath)})`);
}
if (i < uris.length - 1 && uris.length > 1) {
snippet.appendText(options?.separator ?? ' ');
}
});
let label: string;
if (insertedAudioVideoCount > 0) {
if (insertedLinkCount > 0) {
label = vscode.l10n.t('Insert Markdown Media and Links');
} else {
label = vscode.l10n.t('Insert Markdown Media');
}
} else if (insertedImageCount > 0 && insertedLinkCount > 0) {
label = vscode.l10n.t('Insert Markdown Images and Links');
} else if (insertedImageCount > 0) {
label = insertedImageCount > 1
? vscode.l10n.t('Insert Markdown Images')
: vscode.l10n.t('Insert Markdown Image');
} else {
label = insertedLinkCount > 1
? vscode.l10n.t('Insert Markdown Links')
: vscode.l10n.t('Insert Markdown Link');
}
return { snippet, label };
}
/**
* Create a new edit from the image files in a data transfer.
*
* This tries copying files outside of the workspace into the workspace.
*/
export async function createEditForMediaFiles(
document: vscode.TextDocument,
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken
): Promise<{ snippet: vscode.SnippetString; label: string; additionalEdits: vscode.WorkspaceEdit } | undefined> {
if (document.uri.scheme === Schemes.untitled) {
return;
}
interface FileEntry {
readonly uri: vscode.Uri;
readonly newFile?: { readonly contents: vscode.DataTransferFile; readonly overwrite: boolean };
}
const pathGenerator = new NewFilePathGenerator();
const fileEntries = coalesce(await Promise.all(Array.from(dataTransfer, async ([mime, item]): Promise<FileEntry | undefined> => {
if (!mediaMimes.has(mime)) {
return;
}
const file = item?.asFile();
if (!file) {
return;
}
if (file.uri) {
// If the file is already in a workspace, we don't want to create a copy of it
const workspaceFolder = vscode.workspace.getWorkspaceFolder(file.uri);
if (workspaceFolder) {
return { uri: file.uri };
}
}
const newFile = await pathGenerator.getNewFilePath(document, file, token);
if (!newFile) {
return;
}
return { uri: newFile.uri, newFile: { contents: file, overwrite: newFile.overwrite } };
})));
if (!fileEntries.length) {
return;
}
const workspaceEdit = new vscode.WorkspaceEdit();
for (const entry of fileEntries) {
if (entry.newFile) {
workspaceEdit.createFile(entry.uri, {
contents: entry.newFile.contents,
overwrite: entry.newFile.overwrite,
});
}
}
const snippet = createUriListSnippet(document, fileEntries.map(entry => entry.uri));
if (!snippet) {
return;
}
return {
snippet: snippet.snippet,
label: snippet.label,
additionalEdits: workspaceEdit,
};
}
function getMdPath(dir: vscode.Uri | undefined, file: vscode.Uri) {
if (dir && dir.scheme === file.scheme && dir.authority === file.authority) {
if (file.scheme === Schemes.file) {
// On windows, we must use the native `path.relative` to generate the relative path
// so that drive-letters are resolved cast insensitively. However we then want to
// convert back to a posix path to insert in to the document.
const relativePath = path.relative(dir.fsPath, file.fsPath);
return path.posix.normalize(relativePath.split(path.sep).join(path.posix.sep));
}
return path.posix.relative(dir.path, file.path);
}
return file.toString(false);
}
function escapeHtmlAttribute(attr: string): string {
return encodeURI(attr).replaceAll('"', '&quot;');
}
function escapeMarkdownLinkPath(mdPath: string): string {
if (needsBracketLink(mdPath)) {
return '<' + mdPath.replace('<', '\\<').replace('>', '\\>') + '>';
}
return encodeURI(mdPath);
}
function needsBracketLink(mdPath: string) {
// Links with whitespace or control characters must be enclosed in brackets
if (mdPath.startsWith('<') || /\s|[\u007F\u0000-\u001f]/.test(mdPath)) {
return true;
}
// Check if the link has mis-matched parens
if (!/[\(\)]/.test(mdPath)) {
return false;
}
let previousChar = '';
let nestingCount = 0;
for (const char of mdPath) {
if (char === '(' && previousChar !== '\\') {
nestingCount++;
} else if (char === ')' && previousChar !== '\\') {
nestingCount--;
}
if (nestingCount < 0) {
return true;
}
previousChar = char;
}
return nestingCount > 0;
}

View File

@@ -1,29 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { tryGetUriListSnippet } from './dropIntoEditor';
export function registerPasteSupport(selector: vscode.DocumentSelector) {
return vscode.languages.registerDocumentPasteEditProvider(selector, new class implements vscode.DocumentPasteEditProvider {
async provideDocumentPasteEdits(
document: vscode.TextDocument,
_ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken,
): Promise<vscode.DocumentPasteEdit | undefined> {
const enabled = vscode.workspace.getConfiguration('markdown', document).get('experimental.editor.pasteLinks.enabled', true);
if (!enabled) {
return;
}
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
return snippet ? new vscode.DocumentPasteEdit(snippet) : undefined;
}
}, {
pasteMimeTypes: ['text/uri-list']
});
}

View File

@@ -4,10 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { CommandManager } from '../commandManager';
const localize = nls.loadMessageBundle();
// Copied from markdown language service
export enum DiagnosticCode {
@@ -22,18 +20,18 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider {
private static readonly _addToIgnoreLinksCommandId = '_markdown.addToIgnoreLinks';
private static readonly metadata: vscode.CodeActionProviderMetadata = {
private static readonly _metadata: vscode.CodeActionProviderMetadata = {
providedCodeActionKinds: [
vscode.CodeActionKind.QuickFix
],
};
public static register(selector: vscode.DocumentSelector, commandManager: CommandManager): vscode.Disposable {
const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToIgnoreLinksQuickFixProvider(), AddToIgnoreLinksQuickFixProvider.metadata);
const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToIgnoreLinksQuickFixProvider(), AddToIgnoreLinksQuickFixProvider._metadata);
const commandReg = commandManager.register({
id: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId,
execute(resource: vscode.Uri, path: string) {
const settingId = 'experimental.validate.ignoreLinks';
const settingId = 'validate.ignoredLinks';
const config = vscode.workspace.getConfiguration('markdown', resource);
const paths = new Set(config.get<string[]>(settingId, []));
paths.add(path);
@@ -55,7 +53,7 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider {
const hrefText = (diagnostic as any).data?.hrefText;
if (hrefText) {
const fix = new vscode.CodeAction(
localize('ignoreLinksQuickFix.title', "Exclude '{0}' from link validation.", hrefText),
vscode.l10n.t("Exclude '{0}' from link validation.", hrefText),
vscode.CodeActionKind.QuickFix);
fix.command = {

View File

@@ -1,76 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import * as URI from 'vscode-uri';
const imageFileExtensions = new Set<string>([
'.bmp',
'.gif',
'.ico',
'.jpe',
'.jpeg',
'.jpg',
'.png',
'.psd',
'.svg',
'.tga',
'.tif',
'.tiff',
'.webp',
]);
export function registerDropIntoEditorSupport(selector: vscode.DocumentSelector) {
return vscode.languages.registerDocumentDropEditProvider(selector, new class implements vscode.DocumentDropEditProvider {
async provideDocumentDropEdits(document: vscode.TextDocument, _position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.drop.enabled', true);
if (!enabled) {
return undefined;
}
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
return snippet ? new vscode.DocumentDropEdit(snippet) : undefined;
}
});
}
export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.SnippetString | undefined> {
const urlList = await dataTransfer.get('text/uri-list')?.asString();
if (!urlList || token.isCancellationRequested) {
return undefined;
}
const uris: vscode.Uri[] = [];
for (const resource of urlList.split('\n')) {
try {
uris.push(vscode.Uri.parse(resource));
} catch {
// noop
}
}
if (!uris.length) {
return;
}
const snippet = new vscode.SnippetString();
uris.forEach((uri, i) => {
const mdPath = document.uri.scheme === uri.scheme
? encodeURI(path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath).replace(/\\/g, '/'))
: uri.toString(false);
const ext = URI.Utils.extname(uri).toLowerCase();
snippet.appendText(imageFileExtensions.has(ext) ? '![' : '[');
snippet.appendTabstop();
snippet.appendText(`](${mdPath})`);
if (i <= uris.length - 1 && uris.length > 1) {
snippet.appendText(' ');
}
});
return snippet;
}

View File

@@ -4,12 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { BaseLanguageClient } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import type * as lsp from 'vscode-languageserver-types';
import { MdLanguageClient } from '../client/client';
import { Command, CommandManager } from '../commandManager';
import { getReferencesToFileInWorkspace } from '../protocol';
const localize = nls.loadMessageBundle();
export class FindFileReferencesCommand implements Command {
@@ -17,25 +14,22 @@ export class FindFileReferencesCommand implements Command {
public readonly id = 'markdown.findAllFileReferences';
constructor(
private readonly client: BaseLanguageClient,
private readonly _client: MdLanguageClient,
) { }
public async execute(resource?: vscode.Uri) {
resource ??= vscode.window.activeTextEditor?.document.uri;
if (!resource) {
resource = vscode.window.activeTextEditor?.document.uri;
}
if (!resource) {
vscode.window.showErrorMessage(localize('error.noResource', "Find file references failed. No resource provided."));
vscode.window.showErrorMessage(vscode.l10n.t("Find file references failed. No resource provided."));
return;
}
await vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: localize('progress.title', "Finding file references")
title: vscode.l10n.t("Finding file references")
}, async (_progress, token) => {
const locations = (await this.client.sendRequest(getReferencesToFileInWorkspace, { uri: resource!.toString() }, token)).map(loc => {
return new vscode.Location(vscode.Uri.parse(loc.uri), new vscode.Range(loc.range.start.line, loc.range.start.character, loc.range.end.line, loc.range.end.character));
const locations = (await this._client.getReferencesToFileInWorkspace(resource!, token)).map(loc => {
return new vscode.Location(vscode.Uri.parse(loc.uri), convertRange(loc.range));
});
const config = vscode.workspace.getConfiguration('references');
@@ -51,9 +45,13 @@ export class FindFileReferencesCommand implements Command {
}
}
export function convertRange(range: lsp.Range): vscode.Range {
return new vscode.Range(range.start.line, range.start.character, range.end.line, range.end.character);
}
export function registerFindFileReferenceSupport(
commandManager: CommandManager,
client: BaseLanguageClient,
client: MdLanguageClient,
): vscode.Disposable {
return commandManager.register(new FindFileReferencesCommand(client));
}

View File

@@ -0,0 +1,230 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as picomatch from 'picomatch';
import * as vscode from 'vscode';
import { TextDocumentEdit } from 'vscode-languageclient';
import { MdLanguageClient } from '../client/client';
import { Delayer } from '../util/async';
import { noopToken } from '../util/cancellation';
import { Disposable } from '../util/dispose';
import { convertRange } from './fileReferences';
const settingNames = Object.freeze({
enabled: 'updateLinksOnFileMove.enabled',
include: 'updateLinksOnFileMove.include',
enableForDirectories: 'updateLinksOnFileMove.enableForDirectories',
});
const enum UpdateLinksOnFileMoveSetting {
Prompt = 'prompt',
Always = 'always',
Never = 'never',
}
interface RenameAction {
readonly oldUri: vscode.Uri;
readonly newUri: vscode.Uri;
}
class UpdateLinksOnFileRenameHandler extends Disposable {
private readonly _delayer = new Delayer(50);
private readonly _pendingRenames = new Set<RenameAction>();
public constructor(
private readonly _client: MdLanguageClient,
) {
super();
this._register(vscode.workspace.onDidRenameFiles(async (e) => {
await Promise.all(e.files.map(async (rename) => {
if (await this._shouldParticipateInLinkUpdate(rename.newUri)) {
this._pendingRenames.add(rename);
}
}));
if (this._pendingRenames.size) {
this._delayer.trigger(() => {
vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: vscode.l10n.t("Checking for Markdown links to update")
}, () => this._flushRenames());
});
}
}));
}
private async _flushRenames(): Promise<void> {
const renames = Array.from(this._pendingRenames);
this._pendingRenames.clear();
const result = await this._getEditsForFileRename(renames, noopToken);
if (result && result.edit.size) {
if (await this._confirmActionWithUser(result.resourcesBeingRenamed)) {
await vscode.workspace.applyEdit(result.edit);
}
}
}
private async _confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
if (!newResources.length) {
return false;
}
const config = vscode.workspace.getConfiguration('markdown', newResources[0]);
const setting = config.get<UpdateLinksOnFileMoveSetting>(settingNames.enabled);
switch (setting) {
case UpdateLinksOnFileMoveSetting.Prompt:
return this._promptUser(newResources);
case UpdateLinksOnFileMoveSetting.Always:
return true;
case UpdateLinksOnFileMoveSetting.Never:
default:
return false;
}
}
private async _shouldParticipateInLinkUpdate(newUri: vscode.Uri): Promise<boolean> {
const config = vscode.workspace.getConfiguration('markdown', newUri);
const setting = config.get<UpdateLinksOnFileMoveSetting>(settingNames.enabled);
if (setting === UpdateLinksOnFileMoveSetting.Never) {
return false;
}
const externalGlob = config.get<string[]>(settingNames.include);
if (externalGlob) {
for (const glob of externalGlob) {
if (picomatch.isMatch(newUri.fsPath, glob)) {
return true;
}
}
}
const stat = await vscode.workspace.fs.stat(newUri);
if (stat.type === vscode.FileType.Directory) {
return config.get<boolean>(settingNames.enableForDirectories, true);
}
return false;
}
private async _promptUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
if (!newResources.length) {
return false;
}
const rejectItem: vscode.MessageItem = {
title: vscode.l10n.t("No"),
isCloseAffordance: true,
};
const acceptItem: vscode.MessageItem = {
title: vscode.l10n.t("Yes"),
};
const alwaysItem: vscode.MessageItem = {
title: vscode.l10n.t("Always"),
};
const neverItem: vscode.MessageItem = {
title: vscode.l10n.t("Never"),
};
const choice = await vscode.window.showInformationMessage(
newResources.length === 1
? vscode.l10n.t("Update Markdown links for '{0}'?", path.basename(newResources[0].fsPath))
: this._getConfirmMessage(vscode.l10n.t("Update Markdown links for the following {0} files?", newResources.length), newResources), {
modal: true,
}, rejectItem, acceptItem, alwaysItem, neverItem);
switch (choice) {
case acceptItem: {
return true;
}
case rejectItem: {
return false;
}
case alwaysItem: {
const config = vscode.workspace.getConfiguration('markdown', newResources[0]);
config.update(
settingNames.enabled,
UpdateLinksOnFileMoveSetting.Always,
this._getConfigTargetScope(config, settingNames.enabled));
return true;
}
case neverItem: {
const config = vscode.workspace.getConfiguration('markdown', newResources[0]);
config.update(
settingNames.enabled,
UpdateLinksOnFileMoveSetting.Never,
this._getConfigTargetScope(config, settingNames.enabled));
return false;
}
default: {
return false;
}
}
}
private async _getEditsForFileRename(renames: readonly RenameAction[], token: vscode.CancellationToken): Promise<{ edit: vscode.WorkspaceEdit; resourcesBeingRenamed: vscode.Uri[] } | undefined> {
const result = await this._client.getEditForFileRenames(renames.map(rename => ({ oldUri: rename.oldUri.toString(), newUri: rename.newUri.toString() })), token);
if (!result?.edit.documentChanges?.length) {
return undefined;
}
const workspaceEdit = new vscode.WorkspaceEdit();
for (const change of result.edit.documentChanges as TextDocumentEdit[]) {
const uri = vscode.Uri.parse(change.textDocument.uri);
for (const edit of change.edits) {
workspaceEdit.replace(uri, convertRange(edit.range), edit.newText);
}
}
return {
edit: workspaceEdit,
resourcesBeingRenamed: result.participatingRenames.map(x => vscode.Uri.parse(x.newUri)),
};
}
private _getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string {
const MAX_CONFIRM_FILES = 10;
const paths = [start];
paths.push('');
paths.push(...resourcesToConfirm.slice(0, MAX_CONFIRM_FILES).map(r => path.basename(r.fsPath)));
if (resourcesToConfirm.length > MAX_CONFIRM_FILES) {
if (resourcesToConfirm.length - MAX_CONFIRM_FILES === 1) {
paths.push(vscode.l10n.t("...1 additional file not shown"));
} else {
paths.push(vscode.l10n.t("...{0} additional files not shown", resourcesToConfirm.length - MAX_CONFIRM_FILES));
}
}
paths.push('');
return paths.join('\n');
}
private _getConfigTargetScope(config: vscode.WorkspaceConfiguration, settingsName: string): vscode.ConfigurationTarget {
const inspected = config.inspect(settingsName);
if (inspected?.workspaceFolderValue) {
return vscode.ConfigurationTarget.WorkspaceFolder;
}
if (inspected?.workspaceValue) {
return vscode.ConfigurationTarget.Workspace;
}
return vscode.ConfigurationTarget.Global;
}
}
export function registerUpdateLinksOnRename(client: MdLanguageClient): vscode.Disposable {
return new UpdateLinksOnFileRenameHandler(client);
}

View File

@@ -5,7 +5,6 @@
import * as vscode from 'vscode';
import { Disposable } from './util/dispose';
import { lazy } from './util/lazy';
enum Trace {
Off,
@@ -31,49 +30,54 @@ export interface ILogger {
}
export class VsCodeOutputLogger extends Disposable implements ILogger {
private trace?: Trace;
private _trace?: Trace;
private readonly outputChannel = lazy(() => this._register(vscode.window.createOutputChannel('Markdown')));
private _outputChannelValue?: vscode.OutputChannel;
private get _outputChannel() {
this._outputChannelValue ??= this._register(vscode.window.createOutputChannel('Markdown'));
return this._outputChannelValue;
}
constructor() {
super();
this._register(vscode.workspace.onDidChangeConfiguration(() => {
this.updateConfiguration();
this._updateConfiguration();
}));
this.updateConfiguration();
this._updateConfiguration();
}
public verbose(title: string, message: string, data?: any): void {
if (this.trace === Trace.Verbose) {
this.appendLine(`[Verbose ${this.now()}] ${title}: ${message}`);
if (this._trace === Trace.Verbose) {
this._appendLine(`[Verbose ${this._now()}] ${title}: ${message}`);
if (data) {
this.appendLine(VsCodeOutputLogger.data2String(data));
this._appendLine(VsCodeOutputLogger._data2String(data));
}
}
}
private now(): string {
private _now(): string {
const now = new Date();
return String(now.getUTCHours()).padStart(2, '0')
+ ':' + String(now.getMinutes()).padStart(2, '0')
+ ':' + String(now.getUTCSeconds()).padStart(2, '0') + '.' + String(now.getMilliseconds()).padStart(3, '0');
}
private updateConfiguration(): void {
this.trace = this.readTrace();
private _updateConfiguration(): void {
this._trace = this._readTrace();
}
private appendLine(value: string): void {
this.outputChannel.value.appendLine(value);
private _appendLine(value: string): void {
this._outputChannel.appendLine(value);
}
private readTrace(): Trace {
private _readTrace(): Trace {
return Trace.fromString(vscode.workspace.getConfiguration().get<string>('markdown.trace.extension', 'off'));
}
private static data2String(data: any): string {
private static _data2String(data: any): string {
if (data instanceof Error) {
if (typeof data.stack === 'string') {
return data.stack;

View File

@@ -10,14 +10,8 @@ import { ILogger } from './logging';
import { MarkdownContributionProvider } from './markdownExtensions';
import { Slugifier } from './slugify';
import { ITextDocument } from './types/textDocument';
import { Disposable } from './util/dispose';
import { stringHash } from './util/hash';
import { WebviewResourceProvider } from './util/resources';
import { isOfScheme, Schemes } from './util/schemes';
import { MdDocumentInfoCache } from './util/workspaceCache';
import { IMdWorkspace } from './workspace';
const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g;
/**
* Adds begin line index to the output via the 'data-line' data attribute.
@@ -50,47 +44,47 @@ const pluginSourceMap: MarkdownIt.PluginSimple = (md): void => {
type MarkdownItConfig = Readonly<Required<Pick<MarkdownIt.Options, 'breaks' | 'linkify' | 'typographer'>>>;
class TokenCache {
private cachedDocument?: {
private _cachedDocument?: {
readonly uri: vscode.Uri;
readonly version: number;
readonly config: MarkdownItConfig;
};
private tokens?: Token[];
private _tokens?: Token[];
public tryGetCached(document: ITextDocument, config: MarkdownItConfig): Token[] | undefined {
if (this.cachedDocument
&& this.cachedDocument.uri.toString() === document.uri.toString()
&& this.cachedDocument.version === document.version
&& this.cachedDocument.config.breaks === config.breaks
&& this.cachedDocument.config.linkify === config.linkify
if (this._cachedDocument
&& this._cachedDocument.uri.toString() === document.uri.toString()
&& this._cachedDocument.version === document.version
&& this._cachedDocument.config.breaks === config.breaks
&& this._cachedDocument.config.linkify === config.linkify
) {
return this.tokens;
return this._tokens;
}
return undefined;
}
public update(document: ITextDocument, config: MarkdownItConfig, tokens: Token[]) {
this.cachedDocument = {
this._cachedDocument = {
uri: document.uri,
version: document.version,
config,
};
this.tokens = tokens;
this._tokens = tokens;
}
public clean(): void {
this.cachedDocument = undefined;
this.tokens = undefined;
this._cachedDocument = undefined;
this._tokens = undefined;
}
}
export interface RenderOutput {
html: string;
containingImages: { src: string }[];
containingImages: Set<string>;
}
interface RenderEnv {
containingImages: { src: string }[];
containingImages: Set<string>;
currentDocument: vscode.Uri | undefined;
resourceProvider: WebviewResourceProvider | undefined;
}
@@ -103,7 +97,7 @@ export interface IMdParser {
export class MarkdownItEngine implements IMdParser {
private md?: Promise<MarkdownIt>;
private _md?: Promise<MarkdownIt>;
private _slugCount = new Map<string, number>();
private _tokenCache = new TokenCache();
@@ -111,26 +105,26 @@ export class MarkdownItEngine implements IMdParser {
public readonly slugifier: Slugifier;
public constructor(
private readonly contributionProvider: MarkdownContributionProvider,
private readonly _contributionProvider: MarkdownContributionProvider,
slugifier: Slugifier,
private readonly logger: ILogger,
private readonly _logger: ILogger,
) {
this.slugifier = slugifier;
contributionProvider.onContributionsChanged(() => {
_contributionProvider.onContributionsChanged(() => {
// Markdown plugin contributions may have changed
this.md = undefined;
this._md = undefined;
});
}
private async getEngine(config: MarkdownItConfig): Promise<MarkdownIt> {
if (!this.md) {
this.md = (async () => {
private async _getEngine(config: MarkdownItConfig): Promise<MarkdownIt> {
if (!this._md) {
this._md = (async () => {
const markdownIt = await import('markdown-it');
let md: MarkdownIt = markdownIt(await getMarkdownOptions(() => md));
md.linkify.set({ fuzzyLink: false });
for (const plugin of this.contributionProvider.contributions.markdownItPlugins.values()) {
for (const plugin of this._contributionProvider.contributions.markdownItPlugins.values()) {
try {
md = (await plugin)(md);
} catch (e) {
@@ -153,63 +147,63 @@ export class MarkdownItEngine implements IMdParser {
alt: ['paragraph', 'reference', 'blockquote', 'list']
});
this.addImageRenderer(md);
this.addFencedRenderer(md);
this.addLinkNormalizer(md);
this.addLinkValidator(md);
this.addNamedHeaders(md);
this.addLinkRenderer(md);
this._addImageRenderer(md);
this._addFencedRenderer(md);
this._addLinkNormalizer(md);
this._addLinkValidator(md);
this._addNamedHeaders(md);
this._addLinkRenderer(md);
md.use(pluginSourceMap);
return md;
})();
}
const md = await this.md!;
const md = await this._md!;
md.set(config);
return md;
}
public reloadPlugins() {
this.md = undefined;
this._md = undefined;
}
private tokenizeDocument(
private _tokenizeDocument(
document: ITextDocument,
config: MarkdownItConfig,
engine: MarkdownIt
): Token[] {
const cached = this._tokenCache.tryGetCached(document, config);
if (cached) {
this.resetSlugCount();
this._resetSlugCount();
return cached;
}
this.logger.verbose('MarkdownItEngine', `tokenizeDocument - ${document.uri}`);
const tokens = this.tokenizeString(document.getText(), engine);
this._logger.verbose('MarkdownItEngine', `tokenizeDocument - ${document.uri}`);
const tokens = this._tokenizeString(document.getText(), engine);
this._tokenCache.update(document, config, tokens);
return tokens;
}
private tokenizeString(text: string, engine: MarkdownIt) {
this.resetSlugCount();
private _tokenizeString(text: string, engine: MarkdownIt) {
this._resetSlugCount();
return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {});
return engine.parse(text, {});
}
private resetSlugCount(): void {
private _resetSlugCount(): void {
this._slugCount = new Map<string, number>();
}
public async render(input: ITextDocument | string, resourceProvider?: WebviewResourceProvider): Promise<RenderOutput> {
const config = this.getConfig(typeof input === 'string' ? undefined : input.uri);
const engine = await this.getEngine(config);
const config = this._getConfig(typeof input === 'string' ? undefined : input.uri);
const engine = await this._getEngine(config);
const tokens = typeof input === 'string'
? this.tokenizeString(input, engine)
: this.tokenizeDocument(input, config, engine);
? this._tokenizeString(input, engine)
: this._tokenizeDocument(input, config, engine);
const env: RenderEnv = {
containingImages: [],
containingImages: new Set<string>(),
currentDocument: typeof input === 'string' ? undefined : input.uri,
resourceProvider,
};
@@ -226,16 +220,16 @@ export class MarkdownItEngine implements IMdParser {
}
public async tokenize(document: ITextDocument): Promise<Token[]> {
const config = this.getConfig(document.uri);
const engine = await this.getEngine(config);
return this.tokenizeDocument(document, config, engine);
const config = this._getConfig(document.uri);
const engine = await this._getEngine(config);
return this._tokenizeDocument(document, config, engine);
}
public cleanCache(): void {
this._tokenCache.clean();
}
private getConfig(resource?: vscode.Uri): MarkdownItConfig {
private _getConfig(resource?: vscode.Uri): MarkdownItConfig {
const config = vscode.workspace.getConfiguration('markdown', resource ?? null);
return {
breaks: config.get<boolean>('preview.breaks', false),
@@ -244,20 +238,16 @@ export class MarkdownItEngine implements IMdParser {
};
}
private addImageRenderer(md: MarkdownIt): void {
private _addImageRenderer(md: MarkdownIt): void {
const original = md.renderer.rules.image;
md.renderer.rules.image = (tokens: Token[], idx: number, options, env: RenderEnv, self) => {
const token = tokens[idx];
token.attrJoin('class', 'loading');
const src = token.attrGet('src');
if (src) {
env.containingImages?.push({ src });
const imgHash = stringHash(src);
token.attrSet('id', `image-hash-${imgHash}`);
env.containingImages?.add(src);
if (!token.attrGet('data-src')) {
token.attrSet('src', this.toResourceUri(src, env.currentDocument, env.resourceProvider));
token.attrSet('src', this._toResourceUri(src, env.currentDocument, env.resourceProvider));
token.attrSet('data-src', src);
}
}
@@ -270,11 +260,11 @@ export class MarkdownItEngine implements IMdParser {
};
}
private addFencedRenderer(md: MarkdownIt): void {
private _addFencedRenderer(md: MarkdownIt): void {
const original = md.renderer.rules['fenced'];
md.renderer.rules['fenced'] = (tokens: Token[], idx: number, options, env, self) => {
const token = tokens[idx];
if (token.map && token.map.length) {
if (token.map?.length) {
token.attrJoin('class', 'hljs');
}
@@ -286,7 +276,7 @@ export class MarkdownItEngine implements IMdParser {
};
}
private addLinkNormalizer(md: MarkdownIt): void {
private _addLinkNormalizer(md: MarkdownIt): void {
const normalizeLink = md.normalizeLink;
md.normalizeLink = (link: string) => {
try {
@@ -302,7 +292,7 @@ export class MarkdownItEngine implements IMdParser {
};
}
private addLinkValidator(md: MarkdownIt): void {
private _addLinkValidator(md: MarkdownIt): void {
const validateLink = md.validateLink;
md.validateLink = (link: string) => {
return validateLink(link)
@@ -312,7 +302,7 @@ export class MarkdownItEngine implements IMdParser {
};
}
private addNamedHeaders(md: MarkdownIt): void {
private _addNamedHeaders(md: MarkdownIt): void {
const original = md.renderer.rules.heading_open;
md.renderer.rules.heading_open = (tokens: Token[], idx: number, options, env, self) => {
const title = tokens[idx + 1].children!.reduce<string>((acc, t) => acc + t.content, '');
@@ -336,7 +326,7 @@ export class MarkdownItEngine implements IMdParser {
};
}
private addLinkRenderer(md: MarkdownIt): void {
private _addLinkRenderer(md: MarkdownIt): void {
const original = md.renderer.rules.link_open;
md.renderer.rules.link_open = (tokens: Token[], idx: number, options, env, self) => {
@@ -354,7 +344,7 @@ export class MarkdownItEngine implements IMdParser {
};
}
private toResourceUri(href: string, currentDocument: vscode.Uri | undefined, resourceProvider: WebviewResourceProvider | undefined): string {
private _toResourceUri(href: string, currentDocument: vscode.Uri | undefined, resourceProvider: WebviewResourceProvider | undefined): string {
try {
// Support file:// links
if (isOfScheme(Schemes.file, href)) {
@@ -422,6 +412,12 @@ async function getMarkdownOptions(md: () => MarkdownIt): Promise<MarkdownIt.Opti
function normalizeHighlightLang(lang: string | undefined) {
switch (lang && lang.toLowerCase()) {
case 'shell':
return 'sh';
case 'py3':
return 'python';
case 'tsx':
case 'typescriptreact':
// Workaround for highlight not supporting tsx: https://github.com/isagalaev/highlight.js/issues/1155
@@ -439,27 +435,3 @@ function normalizeHighlightLang(lang: string | undefined) {
return lang;
}
}
export class MdParsingProvider extends Disposable implements IMdParser {
private readonly _cache: MdDocumentInfoCache<Token[]>;
public readonly slugifier: Slugifier;
constructor(
engine: MarkdownItEngine,
workspace: IMdWorkspace,
) {
super();
this.slugifier = engine.slugifier;
this._cache = this._register(new MdDocumentInfoCache<Token[]>(workspace, doc => {
return engine.tokenize(doc);
}));
}
public tokenize(document: ITextDocument): Promise<Token[]> {
return this._cache.getForDocument(document);
}
}

View File

@@ -127,7 +127,7 @@ class VSCodeExtensionMarkdownContributionProvider extends Disposable implements
super();
this._register(vscode.extensions.onDidChange(() => {
const currentContributions = this.getCurrentContributions();
const currentContributions = this._getCurrentContributions();
const existingContributions = this._contributions || MarkdownContributions.Empty;
if (!MarkdownContributions.equal(existingContributions, currentContributions)) {
this._contributions = currentContributions;
@@ -144,13 +144,11 @@ class VSCodeExtensionMarkdownContributionProvider extends Disposable implements
public readonly onContributionsChanged = this._onContributionsChanged.event;
public get contributions(): MarkdownContributions {
if (!this._contributions) {
this._contributions = this.getCurrentContributions();
}
this._contributions ??= this._getCurrentContributions();
return this._contributions;
}
private getCurrentContributions(): MarkdownContributions {
private _getCurrentContributions(): MarkdownContributions {
return vscode.extensions.all
.map(MarkdownContributions.fromExtension)
.reduce(MarkdownContributions.merge, MarkdownContributions.Empty);

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as uri from 'vscode-uri';
import { ILogger } from '../logging';
import { MarkdownItEngine } from '../markdownEngine';
@@ -14,7 +13,6 @@ import { WebviewResourceProvider } from '../util/resources';
import { MarkdownPreviewConfiguration, MarkdownPreviewConfigurationManager } from './previewConfig';
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from './security';
const localize = nls.loadMessageBundle();
/**
* Strings used inside the markdown preview.
@@ -23,36 +21,35 @@ const localize = nls.loadMessageBundle();
* can be localized using our normal localization process.
*/
const previewStrings = {
cspAlertMessageText: localize(
'preview.securityMessage.text',
'Some content has been disabled in this document'),
cspAlertMessageText: vscode.l10n.t("Some content has been disabled in this document"),
cspAlertMessageTitle: localize(
'preview.securityMessage.title',
'Potentially unsafe or insecure content has been disabled in the Markdown preview. Change the Markdown preview security setting to allow insecure content or enable scripts'),
cspAlertMessageTitle: vscode.l10n.t("Potentially unsafe or insecure content has been disabled in the Markdown preview. Change the Markdown preview security setting to allow insecure content or enable scripts"),
cspAlertMessageLabel: localize(
'preview.securityMessage.label',
'Content Disabled Security Warning')
cspAlertMessageLabel: vscode.l10n.t("Content Disabled Security Warning")
};
export interface MarkdownContentProviderOutput {
html: string;
containingImages: { src: string }[];
containingImages: Set<string>;
}
export interface ImageInfo {
readonly id: string;
readonly width: number;
readonly height: number;
}
export class MdDocumentRenderer {
constructor(
private readonly engine: MarkdownItEngine,
private readonly context: vscode.ExtensionContext,
private readonly cspArbiter: ContentSecurityPolicyArbiter,
private readonly contributionProvider: MarkdownContributionProvider,
private readonly logger: ILogger
private readonly _engine: MarkdownItEngine,
private readonly _context: vscode.ExtensionContext,
private readonly _cspArbiter: ContentSecurityPolicyArbiter,
private readonly _contributionProvider: MarkdownContributionProvider,
private readonly _logger: ILogger
) {
this.iconPath = {
dark: vscode.Uri.joinPath(this.context.extensionUri, 'media', 'preview-dark.svg'),
light: vscode.Uri.joinPath(this.context.extensionUri, 'media', 'preview-light.svg'),
dark: vscode.Uri.joinPath(this._context.extensionUri, 'media', 'preview-dark.svg'),
light: vscode.Uri.joinPath(this._context.extensionUri, 'media', 'preview-light.svg'),
};
}
@@ -62,8 +59,10 @@ export class MdDocumentRenderer {
markdownDocument: vscode.TextDocument,
resourceProvider: WebviewResourceProvider,
previewConfigurations: MarkdownPreviewConfigurationManager,
initialLine: number | undefined = undefined,
initialLine: number | undefined,
selectedLine: number | undefined,
state: any | undefined,
imageInfo: readonly ImageInfo[],
token: vscode.CancellationToken
): Promise<MarkdownContentProviderOutput> {
const sourceUri = markdownDocument.uri;
@@ -72,27 +71,27 @@ export class MdDocumentRenderer {
source: sourceUri.toString(),
fragment: state?.fragment || markdownDocument.uri.fragment || undefined,
line: initialLine,
lineCount: markdownDocument.lineCount,
selectedLine,
scrollPreviewWithEditor: config.scrollPreviewWithEditor,
scrollEditorWithPreview: config.scrollEditorWithPreview,
doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor,
disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings(),
disableSecurityWarnings: this._cspArbiter.shouldDisableSecurityWarnings(),
webviewResourceRoot: resourceProvider.asWebviewUri(markdownDocument.uri).toString(),
};
this.logger.verbose('DocumentRenderer', `provideTextDocumentContent - ${markdownDocument.uri}`, initialData);
this._logger.verbose('DocumentRenderer', `provideTextDocumentContent - ${markdownDocument.uri}`, initialData);
// Content Security Policy
const nonce = getNonce();
const csp = this.getCsp(resourceProvider, sourceUri, nonce);
const csp = this._getCsp(resourceProvider, sourceUri, nonce);
const body = await this.renderBody(markdownDocument, resourceProvider);
if (token.isCancellationRequested) {
return { html: '', containingImages: [] };
return { html: '', containingImages: new Set() };
}
const html = `<!DOCTYPE html>
<html style="${escapeAttribute(this.getSettingsOverrideStyles(config))}">
<html style="${escapeAttribute(this._getSettingsOverrideStyles(config))}">
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
${csp}
@@ -100,13 +99,13 @@ export class MdDocumentRenderer {
data-settings="${escapeAttribute(JSON.stringify(initialData))}"
data-strings="${escapeAttribute(JSON.stringify(previewStrings))}"
data-state="${escapeAttribute(JSON.stringify(state || {}))}">
<script src="${this.extensionResourcePath(resourceProvider, 'pre.js')}" nonce="${nonce}"></script>
${this.getStyles(resourceProvider, sourceUri, config, state)}
<script src="${this._extensionResourcePath(resourceProvider, 'pre.js')}" nonce="${nonce}"></script>
${this._getStyles(resourceProvider, sourceUri, config, imageInfo)}
<base href="${resourceProvider.asWebviewUri(markdownDocument.uri)}">
</head>
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
${body.html}
${this.getScripts(resourceProvider, nonce)}
${this._getScripts(resourceProvider, nonce)}
</body>
</html>`;
return {
@@ -119,7 +118,7 @@ export class MdDocumentRenderer {
markdownDocument: vscode.TextDocument,
resourceProvider: WebviewResourceProvider,
): Promise<MarkdownContentProviderOutput> {
const rendered = await this.engine.render(markdownDocument, resourceProvider);
const rendered = await this._engine.render(markdownDocument, resourceProvider);
const html = `<div class="markdown-body" dir="auto">${rendered.html}<div class="code-line" data-line="${markdownDocument.lineCount}"></div></div>`;
return {
html,
@@ -129,7 +128,7 @@ export class MdDocumentRenderer {
public renderFileNotFoundDocument(resource: vscode.Uri): string {
const resourcePath = uri.Utils.basename(resource);
const body = localize('preview.notFound', '{0} cannot be found', resourcePath);
const body = vscode.l10n.t('{0} cannot be found', resourcePath);
return `<!DOCTYPE html>
<html>
<body class="vscode-body">
@@ -138,13 +137,13 @@ export class MdDocumentRenderer {
</html>`;
}
private extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string {
private _extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string {
const webviewResource = resourceProvider.asWebviewUri(
vscode.Uri.joinPath(this.context.extensionUri, 'media', mediaFile));
vscode.Uri.joinPath(this._context.extensionUri, 'media', mediaFile));
return webviewResource.toString();
}
private fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string {
private _fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string {
if (!href) {
return href;
}
@@ -168,18 +167,18 @@ export class MdDocumentRenderer {
return resourceProvider.asWebviewUri(vscode.Uri.joinPath(uri.Utils.dirname(resource), href)).toString();
}
private computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
private _computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
if (!Array.isArray(config.styles)) {
return '';
}
const out: string[] = [];
for (const style of config.styles) {
out.push(`<link rel="stylesheet" class="code-user-style" data-source="${escapeAttribute(style)}" href="${escapeAttribute(this.fixHref(resourceProvider, resource, style))}" type="text/css" media="screen">`);
out.push(`<link rel="stylesheet" class="code-user-style" data-source="${escapeAttribute(style)}" href="${escapeAttribute(this._fixHref(resourceProvider, resource, style))}" type="text/css" media="screen">`);
}
return out.join('\n');
}
private getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string {
private _getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string {
return [
config.fontFamily ? `--markdown-font-family: ${config.fontFamily};` : '',
isNaN(config.fontSize) ? '' : `--markdown-font-size: ${config.fontSize}px;`,
@@ -187,35 +186,37 @@ export class MdDocumentRenderer {
].join(' ');
}
private getImageStabilizerStyles(state?: any) {
private _getImageStabilizerStyles(imageInfo: readonly ImageInfo[]): string {
if (!imageInfo.length) {
return '';
}
let ret = '<style>\n';
if (state && state.imageInfo) {
state.imageInfo.forEach((imgInfo: any) => {
ret += `#${imgInfo.id}.loading {
for (const imgInfo of imageInfo) {
ret += `#${imgInfo.id}.loading {
height: ${imgInfo.height}px;
width: ${imgInfo.width}px;
}\n`;
});
}
ret += '</style>\n';
return ret;
}
private getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, state?: any): string {
private _getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, imageInfo: readonly ImageInfo[]): string {
const baseStyles: string[] = [];
for (const resource of this.contributionProvider.contributions.previewStyles) {
for (const resource of this._contributionProvider.contributions.previewStyles) {
baseStyles.push(`<link rel="stylesheet" type="text/css" href="${escapeAttribute(resourceProvider.asWebviewUri(resource))}">`);
}
return `${baseStyles.join('\n')}
${this.computeCustomStyleSheetIncludes(resourceProvider, resource, config)}
${this.getImageStabilizerStyles(state)}`;
${this._computeCustomStyleSheetIncludes(resourceProvider, resource, config)}
${this._getImageStabilizerStyles(imageInfo)}`;
}
private getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string {
private _getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string {
const out: string[] = [];
for (const resource of this.contributionProvider.contributions.previewScripts) {
for (const resource of this._contributionProvider.contributions.previewScripts) {
out.push(`<script async
src="${escapeAttribute(resourceProvider.asWebviewUri(resource))}"
nonce="${nonce}"
@@ -224,13 +225,13 @@ export class MdDocumentRenderer {
return out.join('\n');
}
private getCsp(
private _getCsp(
provider: WebviewResourceProvider,
resource: vscode.Uri,
nonce: string
): string {
const rule = provider.cspSource;
switch (this.cspArbiter.getSecurityLevelForResource(resource)) {
switch (this._cspArbiter.getSecurityLevelForResource(resource)) {
case MarkdownPreviewSecurityLevel.AllowInsecureContent:
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${rule} http: https: data:; media-src 'self' ${rule} http: https: data:; script-src 'nonce-${nonce}'; style-src 'self' ${rule} 'unsafe-inline' http: https: data:; font-src 'self' ${rule} http: https: data:;">`;

View File

@@ -4,107 +4,61 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as uri from 'vscode-uri';
import { ILogger } from '../logging';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { Disposable } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
import { openDocumentLink, resolveDocumentLink, resolveUriToMarkdownFile } from '../util/openDocumentLink';
import { MdLinkOpener } from '../util/openDocumentLink';
import { WebviewResourceProvider } from '../util/resources';
import { urlToUri } from '../util/url';
import { IMdWorkspace } from '../workspace';
import { MdDocumentRenderer } from './documentRenderer';
import { ImageInfo, MdDocumentRenderer } from './documentRenderer';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { scrollEditorToLine, StartingScrollFragment, StartingScrollLine, StartingScrollLocation } from './scrolling';
import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from './topmostLineMonitor';
const localize = nls.loadMessageBundle();
interface WebviewMessage {
readonly source: string;
}
interface CacheImageSizesMessage extends WebviewMessage {
readonly type: 'cacheImageSizes';
readonly body: { id: string; width: number; height: number }[];
}
interface RevealLineMessage extends WebviewMessage {
readonly type: 'revealLine';
readonly body: {
readonly line: number;
};
}
interface DidClickMessage extends WebviewMessage {
readonly type: 'didClick';
readonly body: {
readonly line: number;
};
}
interface ClickLinkMessage extends WebviewMessage {
readonly type: 'openLink';
readonly body: {
readonly href: string;
};
}
interface ShowPreviewSecuritySelectorMessage extends WebviewMessage {
readonly type: 'showPreviewSecuritySelector';
}
interface PreviewStyleLoadErrorMessage extends WebviewMessage {
readonly type: 'previewStyleLoadError';
readonly body: {
readonly unloadedStyles: string[];
};
}
import type { FromWebviewMessage, ToWebviewMessage } from '../../types/previewMessaging';
export class PreviewDocumentVersion {
public readonly resource: vscode.Uri;
private readonly version: number;
private readonly _version: number;
public constructor(document: vscode.TextDocument) {
this.resource = document.uri;
this.version = document.version;
this._version = document.version;
}
public equals(other: PreviewDocumentVersion): boolean {
return this.resource.fsPath === other.resource.fsPath
&& this.version === other.version;
&& this._version === other._version;
}
}
interface MarkdownPreviewDelegate {
getTitle?(resource: vscode.Uri): string;
getAdditionalState(): {};
openPreviewLinkToMarkdownFile(markdownLink: vscode.Uri, fragment: string): void;
openPreviewLinkToMarkdownFile(markdownLink: vscode.Uri, fragment: string | undefined): void;
}
class MarkdownPreview extends Disposable implements WebviewResourceProvider {
private static readonly unwatchedImageSchemes = new Set(['https', 'http', 'data']);
private static readonly _unwatchedImageSchemes = new Set(['https', 'http', 'data']);
private _disposed: boolean = false;
private readonly delay = 300;
private throttleTimer: any;
private readonly _delay = 300;
private _throttleTimer: any;
private readonly _resource: vscode.Uri;
private readonly _webviewPanel: vscode.WebviewPanel;
private line: number | undefined;
private scrollToFragment: string | undefined;
private firstUpdate = true;
private currentVersion?: PreviewDocumentVersion;
private isScrolling = false;
private _line: number | undefined;
private _scrollToFragment: string | undefined;
private _firstUpdate = true;
private _currentVersion?: PreviewDocumentVersion;
private _isScrolling = false;
private imageInfo: { readonly id: string; readonly width: number; readonly height: number }[] = [];
private _imageInfo: readonly ImageInfo[] = [];
private readonly _fileWatchersBySrc = new Map</* src: */ string, vscode.FileSystemWatcher>();
private readonly _onScrollEmitter = this._register(new vscode.EventEmitter<LastScrollLocation>());
@@ -116,13 +70,12 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
webview: vscode.WebviewPanel,
resource: vscode.Uri,
startingScroll: StartingScrollLocation | undefined,
private readonly delegate: MarkdownPreviewDelegate,
private readonly _delegate: MarkdownPreviewDelegate,
private readonly _contentProvider: MdDocumentRenderer,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly _workspace: IMdWorkspace,
private readonly _logger: ILogger,
private readonly _contributionProvider: MarkdownContributionProvider,
private readonly _tocProvider: MdTableOfContentsProvider,
private readonly _opener: MdLinkOpener,
) {
super();
@@ -132,12 +85,12 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
switch (startingScroll?.type) {
case 'line':
if (!isNaN(startingScroll.line!)) {
this.line = startingScroll.line;
this._line = startingScroll.line;
}
break;
case 'fragment':
this.scrollToFragment = startingScroll.fragment;
this._scrollToFragment = startingScroll.fragment;
break;
}
@@ -161,32 +114,32 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
this._register(watcher.onDidChange(uri => {
if (this.isPreviewOf(uri)) {
// Only use the file system event when VS Code does not already know about the file
if (!vscode.workspace.textDocuments.some(doc => doc.uri.toString() !== uri.toString())) {
if (!vscode.workspace.textDocuments.some(doc => doc.uri.toString() === uri.toString())) {
this.refresh();
}
}
}));
this._register(this._webviewPanel.webview.onDidReceiveMessage((e: CacheImageSizesMessage | RevealLineMessage | DidClickMessage | ClickLinkMessage | ShowPreviewSecuritySelectorMessage | PreviewStyleLoadErrorMessage) => {
this._register(this._webviewPanel.webview.onDidReceiveMessage((e: FromWebviewMessage.Type) => {
if (e.source !== this._resource.toString()) {
return;
}
switch (e.type) {
case 'cacheImageSizes':
this.imageInfo = e.body;
this._imageInfo = e.imageData;
break;
case 'revealLine':
this.onDidScrollPreview(e.body.line);
this._onDidScrollPreview(e.line);
break;
case 'didClick':
this.onDidClickPreview(e.body.line);
this._onDidClickPreview(e.line);
break;
case 'openLink':
this.onDidClickPreviewLink(e.body.href);
this._onDidClickPreviewLink(e.href);
break;
case 'showPreviewSecuritySelector':
@@ -195,14 +148,12 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
case 'previewStyleLoadError':
vscode.window.showWarningMessage(
localize('onPreviewStyleLoadError',
"Could not load 'markdown.styles': {0}",
e.body.unloadedStyles.join(', ')));
vscode.l10n.t("Could not load 'markdown.styles': {0}", e.unloadedStyles.join(', ')));
break;
}
}));
this.updatePreview();
this.refresh();
}
override dispose() {
@@ -212,7 +163,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
this._disposed = true;
clearTimeout(this.throttleTimer);
clearTimeout(this._throttleTimer);
for (const entry of this._fileWatchersBySrc.values()) {
entry.dispose();
}
@@ -226,10 +177,9 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
public get state() {
return {
resource: this._resource.toString(),
line: this.line,
imageInfo: this.imageInfo,
fragment: this.scrollToFragment,
...this.delegate.getAdditionalState(),
line: this._line,
fragment: this._scrollToFragment,
...this._delegate.getAdditionalState(),
};
}
@@ -239,15 +189,15 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
*/
public refresh(forceUpdate: boolean = false) {
// Schedule update if none is pending
if (!this.throttleTimer) {
if (this.firstUpdate) {
this.updatePreview(true);
if (!this._throttleTimer) {
if (this._firstUpdate) {
this._updatePreview(true);
} else {
this.throttleTimer = setTimeout(() => this.updatePreview(forceUpdate), this.delay);
this._throttleTimer = setTimeout(() => this._updatePreview(forceUpdate), this._delay);
}
}
this.firstUpdate = false;
this._firstUpdate = false;
}
@@ -255,7 +205,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
return this._resource.fsPath === resource.fsPath;
}
public postMessage(msg: any) {
public postMessage(msg: ToWebviewMessage.Type) {
if (!this._disposed) {
this._webviewPanel.webview.postMessage(msg);
}
@@ -266,13 +216,13 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
return;
}
if (this.isScrolling) {
this.isScrolling = false;
if (this._isScrolling) {
this._isScrolling = false;
return;
}
this._logger.verbose('MarkdownPreview', 'updateForView', { markdownFile: this._resource });
this.line = topLine;
this._line = topLine;
this.postMessage({
type: 'updateView',
line: topLine,
@@ -280,9 +230,9 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
});
}
private async updatePreview(forceUpdate?: boolean): Promise<void> {
clearTimeout(this.throttleTimer);
this.throttleTimer = undefined;
private async _updatePreview(forceUpdate?: boolean): Promise<void> {
clearTimeout(this._throttleTimer);
this._throttleTimer = undefined;
if (this._disposed) {
return;
@@ -293,7 +243,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
document = await vscode.workspace.openTextDocument(this._resource);
} catch {
if (!this._disposed) {
await this.showFileNotFoundError();
await this._showFileNotFoundError();
}
return;
}
@@ -303,31 +253,39 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
}
const pendingVersion = new PreviewDocumentVersion(document);
if (!forceUpdate && this.currentVersion?.equals(pendingVersion)) {
if (this.line) {
this.scrollTo(this.line);
if (!forceUpdate && this._currentVersion?.equals(pendingVersion)) {
if (this._line) {
this.scrollTo(this._line);
}
return;
}
const shouldReloadPage = forceUpdate || !this.currentVersion || this.currentVersion.resource.toString() !== pendingVersion.resource.toString() || !this._webviewPanel.visible;
this.currentVersion = pendingVersion;
const shouldReloadPage = forceUpdate || !this._currentVersion || this._currentVersion.resource.toString() !== pendingVersion.resource.toString() || !this._webviewPanel.visible;
this._currentVersion = pendingVersion;
let selectedLine: number | undefined = undefined;
for (const editor of vscode.window.visibleTextEditors) {
if (this.isPreviewOf(editor.document.uri)) {
selectedLine = editor.selection.active.line;
break;
}
}
const content = await (shouldReloadPage
? this._contentProvider.renderDocument(document, this, this._previewConfigurations, this.line, this.state, this._disposeCts.token)
? this._contentProvider.renderDocument(document, this, this._previewConfigurations, this._line, selectedLine, this.state, this._imageInfo, this._disposeCts.token)
: this._contentProvider.renderBody(document, this));
// Another call to `doUpdate` may have happened.
// Make sure we are still updating for the correct document
if (this.currentVersion?.equals(pendingVersion)) {
this.updateWebviewContent(content.html, shouldReloadPage);
this.updateImageWatchers(content.containingImages);
if (this._currentVersion?.equals(pendingVersion)) {
this._updateWebviewContent(content.html, shouldReloadPage);
this._updateImageWatchers(content.containingImages);
}
}
private onDidScrollPreview(line: number) {
this.line = line;
this._onScrollEmitter.fire({ line: this.line, uri: this._resource });
private _onDidScrollPreview(line: number) {
this._line = line;
this._onScrollEmitter.fire({ line: this._line, uri: this._resource });
const config = this._previewConfigurations.loadAndCacheConfiguration(this._resource);
if (!config.scrollEditorWithPreview) {
return;
@@ -338,12 +296,12 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
continue;
}
this.isScrolling = true;
this._isScrolling = true;
scrollEditorToLine(line, editor);
}
}
private async onDidClickPreview(line: number): Promise<void> {
private async _onDidClickPreview(line: number): Promise<void> {
// fix #82457, find currently opened but unfocused source tab
await vscode.commands.executeCommand('markdown.showSource');
@@ -367,28 +325,28 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
.then((editor) => {
revealLineInEditor(editor);
}, () => {
vscode.window.showErrorMessage(localize('preview.clickOpenFailed', 'Could not open {0}', this._resource.toString()));
vscode.window.showErrorMessage(vscode.l10n.t('Could not open {0}', this._resource.toString()));
});
}
private async showFileNotFoundError() {
private async _showFileNotFoundError() {
this._webviewPanel.webview.html = this._contentProvider.renderFileNotFoundDocument(this._resource);
}
private updateWebviewContent(html: string, reloadPage: boolean): void {
private _updateWebviewContent(html: string, reloadPage: boolean): void {
if (this._disposed) {
return;
}
if (this.delegate.getTitle) {
this._webviewPanel.title = this.delegate.getTitle(this._resource);
if (this._delegate.getTitle) {
this._webviewPanel.title = this._delegate.getTitle(this._resource);
}
this._webviewPanel.webview.options = this.getWebviewOptions();
this._webviewPanel.webview.options = this._getWebviewOptions();
if (reloadPage) {
this._webviewPanel.webview.html = html;
} else {
this._webviewPanel.webview.postMessage({
this.postMessage({
type: 'updateContent',
content: html,
source: this._resource.toString(),
@@ -396,9 +354,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
}
}
private updateImageWatchers(containingImages: { src: string }[]) {
const srcs = new Set(containingImages.map(img => img.src));
private _updateImageWatchers(srcs: Set<string>) {
// Delete stale file watchers.
for (const [src, watcher] of this._fileWatchersBySrc) {
if (!srcs.has(src)) {
@@ -411,7 +367,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
const root = vscode.Uri.joinPath(this._resource, '../');
for (const src of srcs) {
const uri = urlToUri(src, root);
if (uri && !MarkdownPreview.unwatchedImageSchemes.has(uri.scheme) && !this._fileWatchersBySrc.has(src)) {
if (uri && !MarkdownPreview._unwatchedImageSchemes.has(uri.scheme) && !this._fileWatchersBySrc.has(src)) {
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(uri, '*'));
watcher.onDidChange(() => {
this.refresh(true);
@@ -421,15 +377,15 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
}
}
private getWebviewOptions(): vscode.WebviewOptions {
private _getWebviewOptions(): vscode.WebviewOptions {
return {
enableScripts: true,
enableForms: false,
localResourceRoots: this.getLocalResourceRoots()
localResourceRoots: this._getLocalResourceRoots()
};
}
private getLocalResourceRoots(): ReadonlyArray<vscode.Uri> {
private _getLocalResourceRoots(): ReadonlyArray<vscode.Uri> {
const baseRoots = Array.from(this._contributionProvider.contributions.previewResourceRoots);
const folder = vscode.workspace.getWorkspaceFolder(this._resource);
@@ -445,20 +401,24 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
return baseRoots;
}
private async onDidClickPreviewLink(href: string) {
const targetResource = resolveDocumentLink(href, this.resource);
private async _onDidClickPreviewLink(href: string) {
const config = vscode.workspace.getConfiguration('markdown', this.resource);
const openLinks = config.get<string>('preview.openMarkdownLinks', 'inPreview');
if (openLinks === 'inPreview') {
const linkedDoc = await resolveUriToMarkdownFile(this._workspace, targetResource);
if (linkedDoc) {
this.delegate.openPreviewLinkToMarkdownFile(linkedDoc.uri, targetResource.fragment);
return;
const resolved = await this._opener.resolveDocumentLink(href, this.resource);
if (resolved.kind === 'file') {
try {
const doc = await vscode.workspace.openTextDocument(vscode.Uri.from(resolved.uri));
if (isMarkdownFile(doc)) {
return this._delegate.openPreviewLinkToMarkdownFile(doc.uri, resolved.fragment ? decodeURIComponent(resolved.fragment) : undefined);
}
} catch {
// Noop
}
}
}
return openDocumentLink(this._tocProvider, targetResource, this.resource);
return this._opener.openDocumentLink(href, this.resource);
}
//#region WebviewResourceProvider
@@ -504,16 +464,15 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
contentProvider: MdDocumentRenderer,
previewConfigurations: MarkdownPreviewConfigurationManager,
topmostLineMonitor: TopmostLineMonitor,
workspace: IMdWorkspace,
logger: ILogger,
contributionProvider: MarkdownContributionProvider,
tocProvider: MdTableOfContentsProvider,
opener: MdLinkOpener,
scrollLine?: number,
): StaticMarkdownPreview {
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, workspace, logger, contributionProvider, tocProvider, scrollLine);
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, logger, contributionProvider, opener, scrollLine);
}
private readonly preview: MarkdownPreview;
private readonly _preview: MarkdownPreview;
private constructor(
private readonly _webviewPanel: vscode.WebviewPanel,
@@ -521,22 +480,21 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
contentProvider: MdDocumentRenderer,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
topmostLineMonitor: TopmostLineMonitor,
workspace: IMdWorkspace,
logger: ILogger,
contributionProvider: MarkdownContributionProvider,
tocProvider: MdTableOfContentsProvider,
opener: MdLinkOpener,
scrollLine?: number,
) {
super();
const topScrollLocation = scrollLine ? new StartingScrollLine(scrollLine) : undefined;
this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, topScrollLocation, {
this._preview = this._register(new MarkdownPreview(this._webviewPanel, resource, topScrollLocation, {
getAdditionalState: () => { return {}; },
openPreviewLinkToMarkdownFile: (markdownLink, fragment) => {
return vscode.commands.executeCommand('vscode.openWith', markdownLink.with({
fragment
}), StaticMarkdownPreview.customEditorViewType, this._webviewPanel.viewColumn);
}
}, contentProvider, _previewConfigurations, workspace, logger, contributionProvider, tocProvider));
}, contentProvider, _previewConfigurations, logger, contributionProvider, opener));
this._register(this._webviewPanel.onDidDispose(() => {
this.dispose();
@@ -546,13 +504,13 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
this._onDidChangeViewState.fire(e);
}));
this._register(this.preview.onScroll((scrollInfo) => {
this._register(this._preview.onScroll((scrollInfo) => {
topmostLineMonitor.setPreviousStaticEditorLine(scrollInfo);
}));
this._register(topmostLineMonitor.onDidChanged(event => {
if (this.preview.isPreviewOf(event.resource)) {
this.preview.scrollTo(event.line);
if (this._preview.isPreviewOf(event.resource)) {
this._preview.scrollTo(event.line);
}
}));
}
@@ -577,17 +535,17 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
}
public refresh() {
this.preview.refresh(true);
this._preview.refresh(true);
}
public updateConfiguration() {
if (this._previewConfigurations.hasConfigurationChanged(this.preview.resource)) {
if (this._previewConfigurations.hasConfigurationChanged(this._preview.resource)) {
this.refresh();
}
}
public get resource() {
return this.preview.resource;
return this._preview.resource;
}
public get resourceColumn() {
@@ -617,16 +575,15 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
webview: vscode.WebviewPanel,
contentProvider: MdDocumentRenderer,
previewConfigurations: MarkdownPreviewConfigurationManager,
workspace: IMdWorkspace,
logger: ILogger,
topmostLineMonitor: TopmostLineMonitor,
contributionProvider: MarkdownContributionProvider,
tocProvider: MdTableOfContentsProvider,
opener: MdLinkOpener,
): DynamicMarkdownPreview {
webview.iconPath = contentProvider.iconPath;
return new DynamicMarkdownPreview(webview, input,
contentProvider, previewConfigurations, workspace, logger, topmostLineMonitor, contributionProvider, tocProvider);
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, opener);
}
public static create(
@@ -634,21 +591,20 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
previewColumn: vscode.ViewColumn,
contentProvider: MdDocumentRenderer,
previewConfigurations: MarkdownPreviewConfigurationManager,
workspace: IMdWorkspace,
logger: ILogger,
topmostLineMonitor: TopmostLineMonitor,
contributionProvider: MarkdownContributionProvider,
tocProvider: MdTableOfContentsProvider,
opener: MdLinkOpener,
): DynamicMarkdownPreview {
const webview = vscode.window.createWebviewPanel(
DynamicMarkdownPreview.viewType,
DynamicMarkdownPreview.getPreviewTitle(input.resource, input.locked),
DynamicMarkdownPreview._getPreviewTitle(input.resource, input.locked),
previewColumn, { enableFindWidget: true, });
webview.iconPath = contentProvider.iconPath;
return new DynamicMarkdownPreview(webview, input,
contentProvider, previewConfigurations, workspace, logger, topmostLineMonitor, contributionProvider, tocProvider);
contentProvider, previewConfigurations, logger, topmostLineMonitor, contributionProvider, opener);
}
private constructor(
@@ -656,11 +612,10 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
input: DynamicPreviewInput,
private readonly _contentProvider: MdDocumentRenderer,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
private readonly _workspace: IMdWorkspace,
private readonly _logger: ILogger,
private readonly _topmostLineMonitor: TopmostLineMonitor,
private readonly _contributionProvider: MarkdownContributionProvider,
private readonly _tocProvider: MdTableOfContentsProvider,
private readonly _opener: MdLinkOpener,
) {
super();
@@ -669,7 +624,7 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
this._resourceColumn = input.resourceColumn;
this._locked = input.locked;
this._preview = this.createPreview(input.resource, typeof input.line === 'number' ? new StartingScrollLine(input.line) : undefined);
this._preview = this._createPreview(input.resource, typeof input.line === 'number' ? new StartingScrollLine(input.line) : undefined);
this._register(webview.onDidDispose(() => { this.dispose(); }));
@@ -760,19 +715,19 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
}
this._preview.dispose();
this._preview = this.createPreview(newResource, scrollLocation);
this._preview = this._createPreview(newResource, scrollLocation);
}
public toggleLock() {
this._locked = !this._locked;
this._webviewPanel.title = DynamicMarkdownPreview.getPreviewTitle(this._preview.resource, this._locked);
this._webviewPanel.title = DynamicMarkdownPreview._getPreviewTitle(this._preview.resource, this._locked);
}
private static getPreviewTitle(resource: vscode.Uri, locked: boolean): string {
private static _getPreviewTitle(resource: vscode.Uri, locked: boolean): string {
const resourceLabel = uri.Utils.basename(resource);
return locked
? localize('lockedPreviewTitle', '[Preview] {0}', resourceLabel)
: localize('previewTitle', 'Preview {0}', resourceLabel);
? vscode.l10n.t('[Preview] {0}', resourceLabel)
: vscode.l10n.t('Preview {0}', resourceLabel);
}
public get position(): vscode.ViewColumn | undefined {
@@ -799,9 +754,9 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
return this.matchesResource(otherPreview._preview.resource, otherPreview.position, otherPreview._locked);
}
private createPreview(resource: vscode.Uri, startingScroll?: StartingScrollLocation): MarkdownPreview {
private _createPreview(resource: vscode.Uri, startingScroll?: StartingScrollLocation): MarkdownPreview {
return new MarkdownPreview(this._webviewPanel, resource, startingScroll, {
getTitle: (resource) => DynamicMarkdownPreview.getPreviewTitle(resource, this._locked),
getTitle: (resource) => DynamicMarkdownPreview._getPreviewTitle(resource, this._locked),
getAdditionalState: () => {
return {
resourceColumn: this.resourceColumn,
@@ -814,9 +769,8 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
},
this._contentProvider,
this._previewConfigurations,
this._workspace,
this._logger,
this._contributionProvider,
this._tocProvider);
this._opener);
}
}

View File

@@ -65,26 +65,26 @@ export class MarkdownPreviewConfiguration {
}
export class MarkdownPreviewConfigurationManager {
private readonly previewConfigurationsForWorkspaces = new Map<string, MarkdownPreviewConfiguration>();
private readonly _previewConfigurationsForWorkspaces = new Map<string, MarkdownPreviewConfiguration>();
public loadAndCacheConfiguration(
resource: vscode.Uri
): MarkdownPreviewConfiguration {
const config = MarkdownPreviewConfiguration.getForResource(resource);
this.previewConfigurationsForWorkspaces.set(this.getKey(resource), config);
this._previewConfigurationsForWorkspaces.set(this._getKey(resource), config);
return config;
}
public hasConfigurationChanged(
resource: vscode.Uri
): boolean {
const key = this.getKey(resource);
const currentConfig = this.previewConfigurationsForWorkspaces.get(key);
const key = this._getKey(resource);
const currentConfig = this._previewConfigurationsForWorkspaces.get(key);
const newConfig = MarkdownPreviewConfiguration.getForResource(resource);
return (!currentConfig || !currentConfig.isEqualTo(newConfig));
}
private getKey(
private _getKey(
resource: vscode.Uri
): string {
const folder = vscode.workspace.getWorkspaceFolder(resource);

View File

@@ -6,16 +6,16 @@
import * as vscode from 'vscode';
import { ILogger } from '../logging';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { Disposable, disposeAll } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
import { IMdWorkspace } from '../workspace';
import { MdLinkOpener } from '../util/openDocumentLink';
import { MdDocumentRenderer } from './documentRenderer';
import { DynamicMarkdownPreview, IManagedMarkdownPreview, StaticMarkdownPreview } from './preview';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { scrollEditorToLine, StartingScrollFragment } from './scrolling';
import { TopmostLineMonitor } from './topmostLineMonitor';
export interface DynamicPreviewSettings {
readonly resourceColumn: vscode.ViewColumn;
readonly previewColumn: vscode.ViewColumn;
@@ -39,8 +39,9 @@ class PreviewStore<T extends IManagedMarkdownPreview> extends Disposable {
}
public get(resource: vscode.Uri, previewSettings: DynamicPreviewSettings): T | undefined {
const previewColumn = this._resolvePreviewColumn(previewSettings);
for (const preview of this._previews) {
if (preview.matchesResource(resource, previewSettings.previewColumn, previewSettings.locked)) {
if (preview.matchesResource(resource, previewColumn, previewSettings.locked)) {
return preview;
}
}
@@ -54,12 +55,22 @@ class PreviewStore<T extends IManagedMarkdownPreview> extends Disposable {
public delete(preview: T) {
this._previews.delete(preview);
}
private _resolvePreviewColumn(previewSettings: DynamicPreviewSettings): vscode.ViewColumn | undefined {
if (previewSettings.previewColumn === vscode.ViewColumn.Active) {
return vscode.window.tabGroups.activeTabGroup.viewColumn;
}
if (previewSettings.previewColumn === vscode.ViewColumn.Beside) {
return vscode.window.tabGroups.activeTabGroup.viewColumn + 1;
}
return previewSettings.previewColumn;
}
}
export class MarkdownPreviewManager extends Disposable implements vscode.WebviewPanelSerializer, vscode.CustomTextEditorProvider {
private static readonly markdownPreviewActiveContextKey = 'markdownPreviewFocus';
private readonly _topmostLineMonitor = new TopmostLineMonitor();
private readonly _previewConfigurations = new MarkdownPreviewConfigurationManager();
@@ -70,10 +81,9 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
public constructor(
private readonly _contentProvider: MdDocumentRenderer,
private readonly _workspace: IMdWorkspace,
private readonly _logger: ILogger,
private readonly _contributions: MarkdownContributionProvider,
private readonly _tocProvider: MdTableOfContentsProvider,
private readonly _opener: MdLinkOpener,
) {
super();
@@ -120,7 +130,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
if (preview) {
preview.reveal(settings.previewColumn);
} else {
preview = this.createNewDynamicPreview(resource, settings);
preview = this._createNewDynamicPreview(resource, settings);
}
preview.update(
@@ -155,23 +165,58 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
webview: vscode.WebviewPanel,
state: any
): Promise<void> {
const resource = vscode.Uri.parse(state.resource);
const locked = state.locked;
const line = state.line;
const resourceColumn = state.resourceColumn;
try {
const resource = vscode.Uri.parse(state.resource);
const locked = state.locked;
const line = state.line;
const resourceColumn = state.resourceColumn;
const preview = await DynamicMarkdownPreview.revive(
{ resource, locked, line, resourceColumn },
webview,
this._contentProvider,
this._previewConfigurations,
this._workspace,
this._logger,
this._topmostLineMonitor,
this._contributions,
this._tocProvider);
const preview = DynamicMarkdownPreview.revive(
{ resource, locked, line, resourceColumn },
webview,
this._contentProvider,
this._previewConfigurations,
this._logger,
this._topmostLineMonitor,
this._contributions,
this._opener);
this.registerDynamicPreview(preview);
this._registerDynamicPreview(preview);
} catch (e) {
console.error(e);
webview.webview.html = /* html */`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- Disable pinch zooming -->
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<title>Markdown Preview</title>
<style>
html, body {
min-height: 100%;
height: 100%;
}
.error-container {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
</style>
<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
</head>
<body class="error-container">
<p>${vscode.l10n.t("An unexpected error occurred while restoring the Markdown preview.")}</p>
</body>
</html>`;
}
}
public async resolveCustomTextEditor(
@@ -185,16 +230,16 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
this._contentProvider,
this._previewConfigurations,
this._topmostLineMonitor,
this._workspace,
this._logger,
this._contributions,
this._tocProvider,
this._opener,
lineNumber
);
this.registerStaticPreview(preview);
this._registerStaticPreview(preview);
this._activePreview = preview;
}
private createNewDynamicPreview(
private _createNewDynamicPreview(
resource: vscode.Uri,
previewSettings: DynamicPreviewSettings
): DynamicMarkdownPreview {
@@ -210,25 +255,23 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
previewSettings.previewColumn,
this._contentProvider,
this._previewConfigurations,
this._workspace,
this._logger,
this._topmostLineMonitor,
this._contributions,
this._tocProvider);
this._opener);
this.setPreviewActiveContext(true);
this._activePreview = preview;
return this.registerDynamicPreview(preview);
return this._registerDynamicPreview(preview);
}
private registerDynamicPreview(preview: DynamicMarkdownPreview): DynamicMarkdownPreview {
private _registerDynamicPreview(preview: DynamicMarkdownPreview): DynamicMarkdownPreview {
this._dynamicPreviews.add(preview);
preview.onDispose(() => {
this._dynamicPreviews.delete(preview);
});
this.trackActive(preview);
this._trackActive(preview);
preview.onDidChangeViewState(() => {
// Remove other dynamic previews in our column
@@ -237,32 +280,27 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
return preview;
}
private registerStaticPreview(preview: StaticMarkdownPreview): StaticMarkdownPreview {
private _registerStaticPreview(preview: StaticMarkdownPreview): StaticMarkdownPreview {
this._staticPreviews.add(preview);
preview.onDispose(() => {
this._staticPreviews.delete(preview);
});
this.trackActive(preview);
this._trackActive(preview);
return preview;
}
private trackActive(preview: IManagedMarkdownPreview): void {
private _trackActive(preview: IManagedMarkdownPreview): void {
preview.onDidChangeViewState(({ webviewPanel }) => {
this.setPreviewActiveContext(webviewPanel.active);
this._activePreview = webviewPanel.active ? preview : undefined;
});
preview.onDispose(() => {
if (this._activePreview === preview) {
this.setPreviewActiveContext(false);
this._activePreview = undefined;
}
});
}
private setPreviewActiveContext(value: boolean) {
vscode.commands.executeCommand('setContext', MarkdownPreviewManager.markdownPreviewActiveContextKey, value);
}
}

View File

@@ -11,13 +11,21 @@ export function scrollEditorToLine(
line: number,
editor: vscode.TextEditor
) {
const revealRange = toRevealRange(line, editor);
editor.revealRange(revealRange, vscode.TextEditorRevealType.AtTop);
}
function toRevealRange(line: number, editor: vscode.TextEditor): vscode.Range {
line = Math.max(0, line);
const sourceLine = Math.floor(line);
if (sourceLine >= editor.document.lineCount) {
return new vscode.Range(editor.document.lineCount - 1, 0, editor.document.lineCount - 1, 0);
}
const fraction = line - sourceLine;
const text = editor.document.lineAt(sourceLine).text;
const start = Math.floor(fraction * text.length);
editor.revealRange(
new vscode.Range(sourceLine, start, sourceLine + 1, 0),
vscode.TextEditorRevealType.AtTop);
return new vscode.Range(sourceLine, start, sourceLine + 1, 0);
}
export class StartingScrollFragment {

View File

@@ -4,13 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { MarkdownPreviewManager } from './previewManager';
const localize = nls.loadMessageBundle();
export const enum MarkdownPreviewSecurityLevel {
Strict = 0,
AllowInsecureContent = 1,
@@ -31,31 +27,31 @@ export interface ContentSecurityPolicyArbiter {
}
export class ExtensionContentSecurityPolicyArbiter implements ContentSecurityPolicyArbiter {
private readonly old_trusted_workspace_key = 'trusted_preview_workspace:';
private readonly security_level_key = 'preview_security_level:';
private readonly should_disable_security_warning_key = 'preview_should_show_security_warning:';
private readonly _old_trusted_workspace_key = 'trusted_preview_workspace:';
private readonly _security_level_key = 'preview_security_level:';
private readonly _should_disable_security_warning_key = 'preview_should_show_security_warning:';
constructor(
private readonly globalState: vscode.Memento,
private readonly workspaceState: vscode.Memento
private readonly _globalState: vscode.Memento,
private readonly _workspaceState: vscode.Memento
) { }
public getSecurityLevelForResource(resource: vscode.Uri): MarkdownPreviewSecurityLevel {
// Use new security level setting first
const level = this.globalState.get<MarkdownPreviewSecurityLevel | undefined>(this.security_level_key + this.getRoot(resource), undefined);
const level = this._globalState.get<MarkdownPreviewSecurityLevel | undefined>(this._security_level_key + this._getRoot(resource), undefined);
if (typeof level !== 'undefined') {
return level;
}
// Fallback to old trusted workspace setting
if (this.globalState.get<boolean>(this.old_trusted_workspace_key + this.getRoot(resource), false)) {
if (this._globalState.get<boolean>(this._old_trusted_workspace_key + this._getRoot(resource), false)) {
return MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent;
}
return MarkdownPreviewSecurityLevel.Strict;
}
public setSecurityLevelForResource(resource: vscode.Uri, level: MarkdownPreviewSecurityLevel): Thenable<void> {
return this.globalState.update(this.security_level_key + this.getRoot(resource), level);
return this._globalState.update(this._security_level_key + this._getRoot(resource), level);
}
public shouldAllowSvgsForResource(resource: vscode.Uri) {
@@ -64,14 +60,14 @@ export class ExtensionContentSecurityPolicyArbiter implements ContentSecurityPol
}
public shouldDisableSecurityWarnings(): boolean {
return this.workspaceState.get<boolean>(this.should_disable_security_warning_key, false);
return this._workspaceState.get<boolean>(this._should_disable_security_warning_key, false);
}
public setShouldDisableSecurityWarning(disabled: boolean): Thenable<void> {
return this.workspaceState.update(this.should_disable_security_warning_key, disabled);
return this._workspaceState.update(this._should_disable_security_warning_key, disabled);
}
private getRoot(resource: vscode.Uri): vscode.Uri {
private _getRoot(resource: vscode.Uri): vscode.Uri {
if (vscode.workspace.workspaceFolders) {
const folderForResource = vscode.workspace.getWorkspaceFolder(resource);
if (folderForResource) {
@@ -90,8 +86,8 @@ export class ExtensionContentSecurityPolicyArbiter implements ContentSecurityPol
export class PreviewSecuritySelector {
public constructor(
private readonly cspArbiter: ContentSecurityPolicyArbiter,
private readonly webviewManager: MarkdownPreviewManager
private readonly _cspArbiter: ContentSecurityPolicyArbiter,
private readonly _webviewManager: MarkdownPreviewManager
) { }
public async showSecuritySelectorForResource(resource: vscode.Uri): Promise<void> {
@@ -103,40 +99,38 @@ export class PreviewSecuritySelector {
return when ? '• ' : '';
}
const currentSecurityLevel = this.cspArbiter.getSecurityLevelForResource(resource);
const currentSecurityLevel = this._cspArbiter.getSecurityLevelForResource(resource);
const selection = await vscode.window.showQuickPick<PreviewSecurityPickItem>(
[
{
type: MarkdownPreviewSecurityLevel.Strict,
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.Strict) + localize('strict.title', 'Strict'),
description: localize('strict.description', 'Only load secure content'),
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.Strict) + vscode.l10n.t("Strict"),
description: vscode.l10n.t("Only load secure content"),
}, {
type: MarkdownPreviewSecurityLevel.AllowInsecureLocalContent,
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.AllowInsecureLocalContent) + localize('insecureLocalContent.title', 'Allow insecure local content'),
description: localize('insecureLocalContent.description', 'Enable loading content over http served from localhost'),
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.AllowInsecureLocalContent) + vscode.l10n.t("Allow insecure local content"),
description: vscode.l10n.t("Enable loading content over http served from localhost"),
}, {
type: MarkdownPreviewSecurityLevel.AllowInsecureContent,
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.AllowInsecureContent) + localize('insecureContent.title', 'Allow insecure content'),
description: localize('insecureContent.description', 'Enable loading content over http'),
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.AllowInsecureContent) + vscode.l10n.t("Allow insecure content"),
description: vscode.l10n.t("Enable loading content over http"),
}, {
type: MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent,
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent) + localize('disable.title', 'Disable'),
description: localize('disable.description', 'Allow all content and script execution. Not recommended'),
label: markActiveWhen(currentSecurityLevel === MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent) + vscode.l10n.t("Disable"),
description: vscode.l10n.t("Allow all content and script execution. Not recommended"),
}, {
type: 'moreinfo',
label: localize('moreInfo.title', 'More Information'),
label: vscode.l10n.t("More Information"),
description: ''
}, {
type: 'toggle',
label: this.cspArbiter.shouldDisableSecurityWarnings()
? localize('enableSecurityWarning.title', "Enable preview security warnings in this workspace")
: localize('disableSecurityWarning.title', "Disable preview security warning in this workspace"),
description: localize('toggleSecurityWarning.description', 'Does not affect the content security level')
label: this._cspArbiter.shouldDisableSecurityWarnings()
? vscode.l10n.t("Enable preview security warnings in this workspace")
: vscode.l10n.t("Disable preview security warning in this workspace"),
description: vscode.l10n.t("Does not affect the content security level")
},
], {
placeHolder: localize(
'preview.showPreviewSecuritySelector.title',
'Select security settings for Markdown previews in this workspace'),
placeHolder: vscode.l10n.t("Select security settings for Markdown previews in this workspace"),
});
if (!selection) {
return;
@@ -148,12 +142,12 @@ export class PreviewSecuritySelector {
}
if (selection.type === 'toggle') {
this.cspArbiter.setShouldDisableSecurityWarning(!this.cspArbiter.shouldDisableSecurityWarnings());
this.webviewManager.refresh();
this._cspArbiter.setShouldDisableSecurityWarning(!this._cspArbiter.shouldDisableSecurityWarnings());
this._webviewManager.refresh();
return;
} else {
await this.cspArbiter.setSecurityLevelForResource(resource, selection.type);
await this._cspArbiter.setSecurityLevelForResource(resource, selection.type);
}
this.webviewManager.refresh();
this._webviewManager.refresh();
}
}

View File

@@ -15,10 +15,10 @@ export interface LastScrollLocation {
export class TopmostLineMonitor extends Disposable {
private readonly pendingUpdates = new ResourceMap<number>();
private readonly throttle = 50;
private previousTextEditorInfo = new ResourceMap<LastScrollLocation>();
private previousStaticEditorInfo = new ResourceMap<LastScrollLocation>();
private readonly _pendingUpdates = new ResourceMap<number>();
private readonly _throttle = 50;
private _previousTextEditorInfo = new ResourceMap<LastScrollLocation>();
private _previousStaticEditorInfo = new ResourceMap<LastScrollLocation>();
constructor() {
super();
@@ -43,28 +43,28 @@ export class TopmostLineMonitor extends Disposable {
public readonly onDidChanged = this._onChanged.event;
public setPreviousStaticEditorLine(scrollLocation: LastScrollLocation): void {
this.previousStaticEditorInfo.set(scrollLocation.uri, scrollLocation);
this._previousStaticEditorInfo.set(scrollLocation.uri, scrollLocation);
}
public getPreviousStaticEditorLineByUri(resource: vscode.Uri): number | undefined {
const scrollLoc = this.previousStaticEditorInfo.get(resource);
this.previousStaticEditorInfo.delete(resource);
const scrollLoc = this._previousStaticEditorInfo.get(resource);
this._previousStaticEditorInfo.delete(resource);
return scrollLoc?.line;
}
public setPreviousTextEditorLine(scrollLocation: LastScrollLocation): void {
this.previousTextEditorInfo.set(scrollLocation.uri, scrollLocation);
this._previousTextEditorInfo.set(scrollLocation.uri, scrollLocation);
}
public getPreviousTextEditorLineByUri(resource: vscode.Uri): number | undefined {
const scrollLoc = this.previousTextEditorInfo.get(resource);
this.previousTextEditorInfo.delete(resource);
const scrollLoc = this._previousTextEditorInfo.get(resource);
this._previousTextEditorInfo.delete(resource);
return scrollLoc?.line;
}
public getPreviousStaticTextEditorLineByUri(resource: vscode.Uri): number | undefined {
const state = this.previousStaticEditorInfo.get(resource);
const state = this._previousStaticEditorInfo.get(resource);
return state?.line;
}
@@ -72,20 +72,20 @@ export class TopmostLineMonitor extends Disposable {
resource: vscode.Uri,
line: number
) {
if (!this.pendingUpdates.has(resource)) {
if (!this._pendingUpdates.has(resource)) {
// schedule update
setTimeout(() => {
if (this.pendingUpdates.has(resource)) {
if (this._pendingUpdates.has(resource)) {
this._onChanged.fire({
resource,
line: this.pendingUpdates.get(resource) as number
line: this._pendingUpdates.get(resource) as number
});
this.pendingUpdates.delete(resource);
this._pendingUpdates.delete(resource);
}
}, this.throttle);
}, this._throttle);
}
this.pendingUpdates.set(resource, line);
this._pendingUpdates.set(resource, line);
}
}

View File

@@ -24,7 +24,7 @@ export const githubSlugifier: Slugifier = new class implements Slugifier {
.toLowerCase()
.replace(/\s+/g, '-') // Replace whitespace with -
// allow-any-unicode-next-line
.replace(/[\]\[\!\/\'\"\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators
.replace(/[\]\[\!\/\'\"\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators
.replace(/^\-+/, '') // Remove leading -
.replace(/\-+$/, '') // Remove trailing -
);

View File

@@ -1,213 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { ILogger } from './logging';
import { IMdParser } from './markdownEngine';
import { githubSlugifier, Slug, Slugifier } from './slugify';
import { getLine, ITextDocument } from './types/textDocument';
import { Disposable } from './util/dispose';
import { isMarkdownFile } from './util/file';
import { Schemes } from './util/schemes';
import { MdDocumentInfoCache } from './util/workspaceCache';
import { IMdWorkspace } from './workspace';
export interface TocEntry {
readonly slug: Slug;
readonly text: string;
readonly level: number;
readonly line: number;
/**
* The entire range of the header section.
*
* For the doc:
*
* ```md
* # Head #
* text
* # Next head #
* ```
*
* This is the range from `# Head #` to `# Next head #`
*/
readonly sectionLocation: vscode.Location;
/**
* The range of the header declaration.
*
* For the doc:
*
* ```md
* # Head #
* text
* ```
*
* This is the range of `# Head #`
*/
readonly headerLocation: vscode.Location;
/**
* The range of the header text.
*
* For the doc:
*
* ```md
* # Head #
* text
* ```
*
* This is the range of `Head`
*/
readonly headerTextLocation: vscode.Location;
}
export class TableOfContents {
public static async create(parser: IMdParser, document: ITextDocument,): Promise<TableOfContents> {
const entries = await this.buildToc(parser, document);
return new TableOfContents(entries, parser.slugifier);
}
public static async createForDocumentOrNotebook(parser: IMdParser, document: ITextDocument): Promise<TableOfContents> {
if (document.uri.scheme === Schemes.notebookCell) {
const notebook = vscode.workspace.notebookDocuments
.find(notebook => notebook.getCells().some(cell => cell.document === document));
if (notebook) {
return TableOfContents.createForNotebook(parser, notebook);
}
}
return this.create(parser, document);
}
public static async createForNotebook(parser: IMdParser, notebook: vscode.NotebookDocument): Promise<TableOfContents> {
const entries: TocEntry[] = [];
for (const cell of notebook.getCells()) {
if (cell.kind === vscode.NotebookCellKind.Markup && isMarkdownFile(cell.document)) {
entries.push(...(await this.buildToc(parser, cell.document)));
}
}
return new TableOfContents(entries, parser.slugifier);
}
private static async buildToc(parser: IMdParser, document: ITextDocument): Promise<TocEntry[]> {
const toc: TocEntry[] = [];
const tokens = await parser.tokenize(document);
const existingSlugEntries = new Map<string, { count: number }>();
for (const heading of tokens.filter(token => token.type === 'heading_open')) {
if (!heading.map) {
continue;
}
const lineNumber = heading.map[0];
const line = getLine(document, lineNumber);
let slug = parser.slugifier.fromHeading(line);
const existingSlugEntry = existingSlugEntries.get(slug.value);
if (existingSlugEntry) {
++existingSlugEntry.count;
slug = parser.slugifier.fromHeading(slug.value + '-' + existingSlugEntry.count);
} else {
existingSlugEntries.set(slug.value, { count: 0 });
}
const headerLocation = new vscode.Location(document.uri,
new vscode.Range(lineNumber, 0, lineNumber, line.length));
const headerTextLocation = new vscode.Location(document.uri,
new vscode.Range(lineNumber, line.match(/^#+\s*/)?.[0].length ?? 0, lineNumber, line.length - (line.match(/\s*#*$/)?.[0].length ?? 0)));
toc.push({
slug,
text: TableOfContents.getHeaderText(line),
level: TableOfContents.getHeaderLevel(heading.markup),
line: lineNumber,
sectionLocation: headerLocation, // Populated in next steps
headerLocation,
headerTextLocation
});
}
// Get full range of section
return toc.map((entry, startIndex): TocEntry => {
let end: number | undefined = undefined;
for (let i = startIndex + 1; i < toc.length; ++i) {
if (toc[i].level <= entry.level) {
end = toc[i].line - 1;
break;
}
}
const endLine = end ?? document.lineCount - 1;
return {
...entry,
sectionLocation: new vscode.Location(document.uri,
new vscode.Range(
entry.sectionLocation.range.start,
new vscode.Position(endLine, getLine(document, endLine).length)))
};
});
}
private static getHeaderLevel(markup: string): number {
if (markup === '=') {
return 1;
} else if (markup === '-') {
return 2;
} else { // '#', '##', ...
return markup.length;
}
}
private static getHeaderText(header: string): string {
return header.replace(/^\s*#+\s*(.*?)(\s+#+)?$/, (_, word) => word.trim());
}
public static readonly empty = new TableOfContents([], githubSlugifier);
private constructor(
public readonly entries: readonly TocEntry[],
private readonly slugifier: Slugifier,
) { }
public lookup(fragment: string): TocEntry | undefined {
const slug = this.slugifier.fromHeading(fragment);
return this.entries.find(entry => entry.slug.equals(slug));
}
}
export class MdTableOfContentsProvider extends Disposable {
private readonly _cache: MdDocumentInfoCache<TableOfContents>;
constructor(
private readonly parser: IMdParser,
workspace: IMdWorkspace,
private readonly logger: ILogger,
) {
super();
this._cache = this._register(new MdDocumentInfoCache<TableOfContents>(workspace, doc => {
this.logger.verbose('TableOfContentsProvider', `create - ${doc.uri}`);
return TableOfContents.create(parser, doc);
}));
}
public async get(resource: vscode.Uri): Promise<TableOfContents> {
return await this._cache.get(resource) ?? TableOfContents.empty;
}
public getForDocument(doc: ITextDocument): Promise<TableOfContents> {
return this._cache.getForDocument(doc);
}
public createForNotebook(notebook: vscode.NotebookDocument): Promise<TableOfContents> {
return TableOfContents.createForNotebook(this.parser, notebook);
}
}

View File

@@ -29,7 +29,7 @@ class ExtensionReporter implements TelemetryReporter {
constructor(
packageInfo: IPackageInfo
) {
this._reporter = new VSCodeTelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey);
this._reporter = new VSCodeTelemetryReporter(packageInfo.aiKey);
}
sendTelemetryEvent(eventName: string, properties?: {
[key: string]: string;

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* 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 { resolveCopyDestination } from '../languageFeatures/copyFiles/copyFiles';
suite('resolveCopyDestination', () => {
test('Relative destinations should resolve next to document', async () => {
const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md');
{
const dest = resolveCopyDestination(documentUri, 'img.png', '${fileName}', () => vscode.Uri.parse('test://projects/project/'));
assert.strictEqual(dest.toString(), 'test://projects/project/sub/img.png');
}
{
const dest = resolveCopyDestination(documentUri, 'img.png', './${fileName}', () => vscode.Uri.parse('test://projects/project/'));
assert.strictEqual(dest.toString(), 'test://projects/project/sub/img.png');
}
{
const dest = resolveCopyDestination(documentUri, 'img.png', '../${fileName}', () => vscode.Uri.parse('test://projects/project/'));
assert.strictEqual(dest.toString(), 'test://projects/project/img.png');
}
});
test('Destination starting with / should go to workspace root', async () => {
const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md');
const dest = resolveCopyDestination(documentUri, 'img.png', '/${fileName}', () => vscode.Uri.parse('test://projects/project/'));
assert.strictEqual(dest.toString(), 'test://projects/project/img.png');
});
test('If there is no workspace root, / should resolve to document dir', async () => {
const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md');
const dest = resolveCopyDestination(documentUri, 'img.png', '/${fileName}', () => undefined);
assert.strictEqual(dest.toString(), 'test://projects/project/sub/img.png');
});
test('If path ends in /, we should automatically add the fileName', async () => {
{
const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md');
const dest = resolveCopyDestination(documentUri, 'img.png', 'images/', () => vscode.Uri.parse('test://projects/project/'));
assert.strictEqual(dest.toString(), 'test://projects/project/sub/images/img.png');
}
{
const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md');
const dest = resolveCopyDestination(documentUri, 'img.png', './', () => vscode.Uri.parse('test://projects/project/'));
assert.strictEqual(dest.toString(), 'test://projects/project/sub/img.png');
}
{
const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md');
const dest = resolveCopyDestination(documentUri, 'img.png', '/', () => vscode.Uri.parse('test://projects/project/'));
assert.strictEqual(dest.toString(), 'test://projects/project/img.png');
}
});
test('Basic transform', async () => {
const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md');
const dest = resolveCopyDestination(documentUri, 'img.png', '${fileName/.png/.gif/}', () => undefined);
assert.strictEqual(dest.toString(), 'test://projects/project/sub/img.gif');
});
test('transforms should support capture groups', async () => {
const documentUri = vscode.Uri.parse('test://projects/project/sub/readme.md');
const dest = resolveCopyDestination(documentUri, 'img.png', '${fileName/(.+)\\.(.+)/$2.$1/}', () => undefined);
assert.strictEqual(dest.toString(), 'test://projects/project/sub/png.img');
});
});

View File

@@ -134,7 +134,7 @@ async function getLinksForFile(file: vscode.Uri): Promise<vscode.DocumentLink[]>
}
});
test('Should navigate to fragment within current untitled file', async () => { // TODO: skip for now for ls migration
test.skip('Should navigate to fragment within current untitled file', async () => { // TODO: skip for now for ls migration
const testFile = workspaceFile('x.md').with({ scheme: 'untitled' });
await withFileContents(testFile, joinLines(
'[](#second)',
@@ -169,9 +169,8 @@ async function withFileContents(file: vscode.Uri, contents: string): Promise<voi
}
async function executeLink(link: vscode.DocumentLink) {
debugLog('executeingLink', link.target?.toString(), Date.now());
debugLog('executingLink', link.target?.toString(), Date.now());
const args: any[] = JSON.parse(decodeURIComponent(link.target!.query));
await vscode.commands.executeCommand(link.target!.path, vscode.Uri.from(args[0]), ...args.slice(1));
await vscode.commands.executeCommand('vscode.open', link.target!);
debugLog('executedLink', vscode.window.activeTextEditor?.document.toString(), Date.now());
}

View File

@@ -6,7 +6,7 @@
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { InMemoryDocument } from '../util/inMemoryDocument';
import { InMemoryDocument } from '../client/inMemoryDocument';
import { createNewMarkdownEngine } from './engine';
@@ -35,17 +35,18 @@ suite('markdown.engine', () => {
test('Extracts all images', async () => {
const engine = createNewMarkdownEngine();
assert.deepStrictEqual((await engine.render(input)), {
html: '<p data-line="0" class="code-line" dir="auto">'
+ '<img src="img.png" alt="" class="loading" id="image-hash--754511435" data-src="img.png"> '
+ '<a href="no-img.png" data-href="no-img.png"></a> '
+ '<img src="http://example.org/img.png" alt="" class="loading" id="image-hash--1903814170" data-src="http://example.org/img.png"> '
+ '<img src="img.png" alt="" class="loading" id="image-hash--754511435" data-src="img.png"> '
+ '<img src="./img2.png" alt="" class="loading" id="image-hash-265238964" data-src="./img2.png">'
+ '</p>\n'
,
containingImages: [{ src: 'img.png' }, { src: 'http://example.org/img.png' }, { src: 'img.png' }, { src: './img2.png' }],
});
const result = await engine.render(input);
assert.deepStrictEqual(result.html,
'<p data-line="0" class="code-line" dir="auto">'
+ '<img src="img.png" alt="" data-src="img.png"> '
+ '<a href="no-img.png" data-href="no-img.png"></a> '
+ '<img src="http://example.org/img.png" alt="" data-src="http://example.org/img.png"> '
+ '<img src="img.png" alt="" data-src="img.png"> '
+ '<img src="./img2.png" alt="" data-src="./img2.png">'
+ '</p>\n'
);
assert.deepStrictEqual([...result.containingImages], ['img.png', 'http://example.org/img.png', './img2.png']);
});
});
});

View File

@@ -7,13 +7,18 @@ import * as vscode from 'vscode';
import { MarkdownItEngine } from '../markdownEngine';
import { MarkdownContributionProvider, MarkdownContributions } from '../markdownExtensions';
import { githubSlugifier } from '../slugify';
import { Disposable } from '../util/dispose';
import { nulLogger } from './nulLogging';
const emptyContributions = new class extends Disposable implements MarkdownContributionProvider {
const emptyContributions = new class implements MarkdownContributionProvider {
readonly extensionUri = vscode.Uri.file('/');
readonly contributions = MarkdownContributions.Empty;
readonly onContributionsChanged = this._register(new vscode.EventEmitter<this>()).event;
private readonly _onContributionsChanged = new vscode.EventEmitter<this>();
readonly onContributionsChanged = this._onContributionsChanged.event;
dispose() {
this._onContributionsChanged.dispose();
}
};
export function createNewMarkdownEngine(): MarkdownItEngine {

View File

@@ -1,83 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 * as path from 'path';
import * as vscode from 'vscode';
import { ITextDocument } from '../types/textDocument';
import { Disposable } from '../util/dispose';
import { ResourceMap } from '../util/resourceMap';
import { IMdWorkspace } from '../workspace';
export class InMemoryMdWorkspace extends Disposable implements IMdWorkspace {
private readonly _documents = new ResourceMap<ITextDocument>(uri => uri.fsPath);
constructor(documents: ITextDocument[]) {
super();
for (const doc of documents) {
this._documents.set(doc.uri, doc);
}
}
public values() {
return Array.from(this._documents.values());
}
public async getAllMarkdownDocuments() {
return this.values();
}
public async getOrLoadMarkdownDocument(resource: vscode.Uri): Promise<ITextDocument | undefined> {
return this._documents.get(resource);
}
public hasMarkdownDocument(resolvedHrefPath: vscode.Uri): boolean {
return this._documents.has(resolvedHrefPath);
}
public async pathExists(resource: vscode.Uri): Promise<boolean> {
return this._documents.has(resource);
}
public async readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]> {
const files = new Map<string, vscode.FileType>();
const pathPrefix = resource.fsPath + (resource.fsPath.endsWith('/') || resource.fsPath.endsWith('\\') ? '' : path.sep);
for (const doc of this._documents.values()) {
const path = doc.uri.fsPath;
if (path.startsWith(pathPrefix)) {
const parts = path.slice(pathPrefix.length).split(/\/|\\/g);
files.set(parts[0], parts.length > 1 ? vscode.FileType.Directory : vscode.FileType.File);
}
}
return Array.from(files.entries());
}
private readonly _onDidChangeMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<ITextDocument>());
public onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocumentEmitter.event;
private readonly _onDidCreateMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<ITextDocument>());
public onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocumentEmitter.event;
private readonly _onDidDeleteMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<vscode.Uri>());
public onDidDeleteMarkdownDocument = this._onDidDeleteMarkdownDocumentEmitter.event;
public updateDocument(document: ITextDocument) {
this._documents.set(document.uri, document);
this._onDidChangeMarkdownDocumentEmitter.fire(document);
}
public createDocument(document: ITextDocument) {
assert.ok(!this._documents.has(document.uri));
this._documents.set(document.uri, document);
this._onDidCreateMarkdownDocumentEmitter.fire(document);
}
public deleteDocument(resource: vscode.Uri) {
this._documents.delete(resource);
this._onDidDeleteMarkdownDocumentEmitter.fire(resource);
}
}

View File

@@ -3,10 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const testRunner = require('../../../../test/integration/electron/testrunner');
import * as path from 'path';
import * as testRunner from '../../../../test/integration/electron/testrunner';
const options: any = {
const options: import('mocha').MochaOptions = {
ui: 'tdd',
color: true,
timeout: 60000

View File

@@ -2,33 +2,7 @@
* 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 * as os from 'os';
import * as vscode from 'vscode';
import { DisposableStore } from '../util/dispose';
export const joinLines = (...args: string[]) =>
args.join(os.platform() === 'win32' ? '\r\n' : '\n');
export function workspacePath(...segments: string[]): vscode.Uri {
return vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, ...segments);
}
export function assertRangeEqual(expected: vscode.Range, actual: vscode.Range, message?: string) {
assert.strictEqual(expected.start.line, actual.start.line, message);
assert.strictEqual(expected.start.character, actual.start.character, message);
assert.strictEqual(expected.end.line, actual.end.line, message);
assert.strictEqual(expected.end.character, actual.end.character, message);
}
export function withStore<R>(fn: (this: Mocha.Context, store: DisposableStore) => Promise<R>) {
return async function (this: Mocha.Context): Promise<R> {
const store = new DisposableStore();
try {
return await fn.call(this, store);
} finally {
store.dispose();
}
};
}

View File

@@ -11,12 +11,7 @@ import * as vscode from 'vscode';
export interface ITextDocument {
readonly uri: vscode.Uri;
readonly version: number;
readonly lineCount: number;
getText(range?: vscode.Range): string;
positionAt(offset: number): vscode.Position;
getText(): string;
}
export function getLine(doc: ITextDocument, line: number): string {
return doc.getText(new vscode.Range(line, 0, line, Number.MAX_VALUE)).replace(/\r?\n$/, '');
}

View File

@@ -2,6 +2,12 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* @returns New array with all falsy values removed. The original array IS NOT modified.
*/
export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
return <T[]>array.filter(e => !!e);
}
export function equals<T>(one: ReadonlyArray<T>, other: ReadonlyArray<T>, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean {
if (one.length !== other.length) {
@@ -16,10 +22,3 @@ export function equals<T>(one: ReadonlyArray<T>, other: ReadonlyArray<T>, itemEq
return true;
}
/**
* @returns New array with all falsy values removed. The original array IS NOT modified.
*/
export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
return <T[]>array.filter(e => !!e);
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vscode';
export interface ITask<T> {
(): T;
}
@@ -12,65 +10,55 @@ export interface ITask<T> {
export class Delayer<T> {
public defaultDelay: number;
private timeout: any; // Timer
private completionPromise: Promise<T | null> | null;
private onSuccess: ((value: T | PromiseLike<T> | undefined) => void) | null;
private task: ITask<T> | null;
private _timeout: any; // Timer
private _cancelTimeout: Promise<T | null> | null;
private _onSuccess: ((value: T | PromiseLike<T> | undefined) => void) | null;
private _task: ITask<T> | null;
constructor(defaultDelay: number) {
this.defaultDelay = defaultDelay;
this.timeout = null;
this.completionPromise = null;
this.onSuccess = null;
this.task = null;
this._timeout = null;
this._cancelTimeout = null;
this._onSuccess = null;
this._task = null;
}
dispose() {
this.cancelTimeout();
this._doCancelTimeout();
}
public trigger(task: ITask<T>, delay: number = this.defaultDelay): Promise<T | null> {
this.task = task;
this._task = task;
if (delay >= 0) {
this.cancelTimeout();
this._doCancelTimeout();
}
if (!this.completionPromise) {
this.completionPromise = new Promise<T | undefined>((resolve) => {
this.onSuccess = resolve;
if (!this._cancelTimeout) {
this._cancelTimeout = new Promise<T | undefined>((resolve) => {
this._onSuccess = resolve;
}).then(() => {
this.completionPromise = null;
this.onSuccess = null;
const result = this.task && this.task();
this.task = null;
this._cancelTimeout = null;
this._onSuccess = null;
const result = this._task && this._task?.();
this._task = null;
return result;
});
}
if (delay >= 0 || this.timeout === null) {
this.timeout = setTimeout(() => {
this.timeout = null;
this.onSuccess?.(undefined);
if (delay >= 0 || this._timeout === null) {
this._timeout = setTimeout(() => {
this._timeout = null;
this._onSuccess?.(undefined);
}, delay >= 0 ? delay : this.defaultDelay);
}
return this.completionPromise;
return this._cancelTimeout;
}
private cancelTimeout(): void {
if (this.timeout !== null) {
clearTimeout(this.timeout);
this.timeout = null;
private _doCancelTimeout(): void {
if (this._timeout !== null) {
clearTimeout(this._timeout);
this._timeout = null;
}
}
}
export function setImmediate(callback: (...args: any[]) => void, ...args: any[]): Disposable {
if (global.setImmediate) {
const handle = global.setImmediate(callback, ...args);
return { dispose: () => global.clearImmediate(handle) };
} else {
const handle = setTimeout(callback, 0, ...args);
return { dispose: () => clearTimeout(handle) };
}
}

View File

@@ -5,8 +5,8 @@
import * as vscode from 'vscode';
export const noopToken = new class implements vscode.CancellationToken {
_onCancellationRequestedEmitter = new vscode.EventEmitter<void>();
export const noopToken: vscode.CancellationToken = new class implements vscode.CancellationToken {
private readonly _onCancellationRequestedEmitter = new vscode.EventEmitter<void>();
onCancellationRequested = this._onCancellationRequestedEmitter.event;
get isCancellationRequested() { return false; }

View File

@@ -5,14 +5,6 @@
import * as vscode from 'vscode';
export class MultiDisposeError extends Error {
constructor(
public readonly errors: any[]
) {
super(`Encountered errors while disposing of store. Errors: [${errors.join(', ')}]`);
}
}
export function disposeAll(disposables: Iterable<vscode.Disposable>) {
const errors: any[] = [];
@@ -27,7 +19,7 @@ export function disposeAll(disposables: Iterable<vscode.Disposable>) {
if (errors.length === 1) {
throw errors[0];
} else if (errors.length > 1) {
throw new MultiDisposeError(errors);
throw new AggregateError(errors, 'Encountered errors while disposing of store');
}
}
@@ -61,22 +53,3 @@ export abstract class Disposable {
return this._isDisposed;
}
}
export class DisposableStore extends Disposable {
private readonly items = new Set<IDisposable>();
public override dispose() {
super.dispose();
disposeAll(this.items);
this.items.clear();
}
public add<T extends IDisposable>(item: T): T {
if (this.isDisposed) {
console.warn('Adding to disposed store. Item will be leaked');
}
this.items.add(item);
return item;
}
}

View File

@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* 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 { Schemes } from './schemes';
import { Utils } from 'vscode-uri';
export function getDocumentDir(document: vscode.TextDocument): vscode.Uri | undefined {
const docUri = getParentDocumentUri(document);
if (docUri.scheme === Schemes.untitled) {
return vscode.workspace.workspaceFolders?.[0]?.uri;
}
return Utils.dirname(docUri);
}
export function getParentDocumentUri(document: vscode.TextDocument): vscode.Uri {
if (document.uri.scheme === Schemes.notebookCell) {
for (const notebook of vscode.workspace.notebookDocuments) {
for (const cell of notebook.getCells()) {
if (cell.document === document) {
return notebook.uri;
}
}
}
}
return document.uri;
}

View File

@@ -5,6 +5,7 @@
import * as vscode from 'vscode';
import * as URI from 'vscode-uri';
import { Schemes } from './schemes';
export const markdownFileExtensions = Object.freeze<string[]>([
'md',
@@ -22,6 +23,22 @@ export function isMarkdownFile(document: vscode.TextDocument) {
return document.languageId === 'markdown';
}
export function looksLikeMarkdownPath(resolvedHrefPath: vscode.Uri) {
export function looksLikeMarkdownPath(resolvedHrefPath: vscode.Uri): boolean {
const doc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === resolvedHrefPath.toString());
if (doc) {
return isMarkdownFile(doc);
}
if (resolvedHrefPath.scheme === Schemes.notebookCell) {
for (const notebook of vscode.workspace.notebookDocuments) {
for (const cell of notebook.getCells()) {
if (cell.kind === vscode.NotebookCellKind.Markup && isMarkdownFile(cell.document)) {
return true;
}
}
}
return false;
}
return markdownFileExtensions.includes(URI.Utils.extname(resolvedHrefPath).toLowerCase().replace('.', ''));
}

View File

@@ -1,16 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function numberHash(val: number, initialHashVal: number): number {
return (((initialHashVal << 5) - initialHashVal) + val) | 0; // hashVal * 31 + ch, keep as int32
}
export function stringHash(s: string) {
let hashVal = numberHash(149417, 0);
for (let i = 0, length = s.length; i < length; i++) {
hashVal = numberHash(s.charCodeAt(i), hashVal);
}
return hashVal;
}

View File

@@ -1,34 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { TextDocument } from 'vscode-languageserver-textdocument';
import { ITextDocument } from '../types/textDocument';
export class InMemoryDocument implements ITextDocument {
private readonly _doc: TextDocument;
constructor(
public readonly uri: vscode.Uri, contents: string,
public readonly version = 0,
) {
this._doc = TextDocument.create(uri.toString(), 'markdown', version, contents);
}
get lineCount(): number {
return this._doc.lineCount;
}
positionAt(offset: number): vscode.Position {
const pos = this._doc.positionAt(offset);
return new vscode.Position(pos.line, pos.character);
}
getText(range?: vscode.Range): string {
return this._doc.getText(range);
}
}

View File

@@ -1,39 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface Lazy<T> {
readonly value: T;
readonly hasValue: boolean;
map<R>(f: (x: T) => R): Lazy<R>;
}
class LazyValue<T> implements Lazy<T> {
private _hasValue: boolean = false;
private _value?: T;
constructor(
private readonly _getValue: () => T
) { }
get value(): T {
if (!this._hasValue) {
this._hasValue = true;
this._value = this._getValue();
}
return this._value!;
}
get hasValue(): boolean {
return this._hasValue;
}
public map<R>(f: (x: T) => R): Lazy<R> {
return new LazyValue(() => f(this.value));
}
}
export function lazy<T>(getValue: () => T): Lazy<T> {
return new LazyValue<T>(getValue);
}

View File

@@ -1,67 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
interface ILimitedTaskFactory<T> {
factory: ITask<Promise<T>>;
c: (value: T | Promise<T>) => void;
e: (error?: unknown) => void;
}
interface ITask<T> {
(): T;
}
/**
* A helper to queue N promises and run them all with a max degree of parallelism. The helper
* ensures that at any time no more than M promises are running at the same time.
*
* Taken from 'src/vs/base/common/async.ts'
*/
export class Limiter<T> {
private _size = 0;
private runningPromises: number;
private readonly maxDegreeOfParalellism: number;
private readonly outstandingPromises: ILimitedTaskFactory<T>[];
constructor(maxDegreeOfParalellism: number) {
this.maxDegreeOfParalellism = maxDegreeOfParalellism;
this.outstandingPromises = [];
this.runningPromises = 0;
}
get size(): number {
return this._size;
}
queue(factory: ITask<Promise<T>>): Promise<T> {
this._size++;
return new Promise<T>((c, e) => {
this.outstandingPromises.push({ factory, c, e });
this.consume();
});
}
private consume(): void {
while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
const iLimitedTask = this.outstandingPromises.shift()!;
this.runningPromises++;
const promise = iLimitedTask.factory();
promise.then(iLimitedTask.c, iLimitedTask.e);
promise.then(() => this.consumed(), () => this.consumed());
}
}
private consumed(): void {
this._size--;
this.runningPromises--;
if (this.outstandingPromises.length > 0) {
this.consume();
}
}
}

View File

@@ -3,101 +3,59 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import * as uri from 'vscode-uri';
import { MdTableOfContentsProvider } from '../tableOfContents';
import { ITextDocument } from '../types/textDocument';
import { IMdWorkspace } from '../workspace';
import { isMarkdownFile } from './file';
export interface OpenDocumentLinkArgs {
readonly parts: vscode.Uri;
readonly fragment: string;
readonly fromResource: vscode.Uri;
}
import { MdLanguageClient } from '../client/client';
import * as proto from '../client/protocol';
enum OpenMarkdownLinks {
beside = 'beside',
currentGroup = 'currentGroup',
}
export function resolveDocumentLink(href: string, markdownFile: vscode.Uri): vscode.Uri {
const [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c));
export class MdLinkOpener {
if (hrefPath[0] === '/') {
// Absolute path. Try to resolve relative to the workspace
const workspace = vscode.workspace.getWorkspaceFolder(markdownFile);
if (workspace) {
return vscode.Uri.joinPath(workspace.uri, hrefPath.slice(1)).with({ fragment });
constructor(
private readonly _client: MdLanguageClient,
) { }
public async resolveDocumentLink(linkText: string, fromResource: vscode.Uri): Promise<proto.ResolvedDocumentLinkTarget> {
return this._client.resolveLinkTarget(linkText, fromResource);
}
public async openDocumentLink(linkText: string, fromResource: vscode.Uri, viewColumn?: vscode.ViewColumn): Promise<void> {
const resolved = await this._client.resolveLinkTarget(linkText, fromResource);
if (!resolved) {
return;
}
}
// Relative path. Resolve relative to the md file
const dirnameUri = markdownFile.with({ path: path.dirname(markdownFile.path) });
return vscode.Uri.joinPath(dirnameUri, hrefPath).with({ fragment });
}
const uri = vscode.Uri.from(resolved.uri);
switch (resolved.kind) {
case 'external':
return vscode.commands.executeCommand('vscode.open', uri);
export async function openDocumentLink(tocProvider: MdTableOfContentsProvider, targetResource: vscode.Uri, fromResource: vscode.Uri): Promise<void> {
const column = getViewColumn(fromResource);
case 'folder':
return vscode.commands.executeCommand('revealInExplorer', uri);
if (await tryNavigateToFragmentInActiveEditor(tocProvider, targetResource)) {
return;
}
let targetResourceStat: vscode.FileStat | undefined;
try {
targetResourceStat = await vscode.workspace.fs.stat(targetResource);
} catch {
// noop
}
if (typeof targetResourceStat === 'undefined') {
// We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead
if (uri.Utils.extname(targetResource) === '') {
const dotMdResource = targetResource.with({ path: targetResource.path + '.md' });
try {
const stat = await vscode.workspace.fs.stat(dotMdResource);
if (stat.type === vscode.FileType.File) {
await tryOpenMdFile(tocProvider, dotMdResource, column);
return;
case 'file': {
// If no explicit viewColumn is given, check if the editor is already open in a tab
if (typeof viewColumn === 'undefined') {
for (const tab of vscode.window.tabGroups.all.flatMap(x => x.tabs)) {
if (tab.input instanceof vscode.TabInputText) {
if (tab.input.uri.fsPath === uri.fsPath) {
viewColumn = tab.group.viewColumn;
break;
}
}
}
}
} catch {
// noop
return vscode.commands.executeCommand('vscode.open', uri, <vscode.TextDocumentShowOptions>{
selection: resolved.position ? new vscode.Range(resolved.position.line, resolved.position.character, resolved.position.line, resolved.position.character) : undefined,
viewColumn: viewColumn ?? getViewColumn(fromResource),
});
}
}
} else if (targetResourceStat.type === vscode.FileType.Directory) {
return vscode.commands.executeCommand('revealInExplorer', targetResource);
}
await tryOpenMdFile(tocProvider, targetResource, column);
}
async function tryOpenMdFile(tocProvider: MdTableOfContentsProvider, resource: vscode.Uri, column: vscode.ViewColumn): Promise<boolean> {
await vscode.commands.executeCommand('vscode.open', resource.with({ fragment: '' }), column);
return tryNavigateToFragmentInActiveEditor(tocProvider, resource);
}
async function tryNavigateToFragmentInActiveEditor(tocProvider: MdTableOfContentsProvider, resource: vscode.Uri): Promise<boolean> {
const notebookEditor = vscode.window.activeNotebookEditor;
if (notebookEditor?.notebook.uri.fsPath === resource.fsPath) {
if (await tryRevealLineInNotebook(tocProvider, notebookEditor, resource.fragment)) {
return true;
}
}
const activeEditor = vscode.window.activeTextEditor;
if (activeEditor?.document.uri.fsPath === resource.fsPath) {
if (isMarkdownFile(activeEditor.document)) {
if (await tryRevealLineUsingTocFragment(tocProvider, activeEditor, resource.fragment)) {
return true;
}
}
tryRevealLineUsingLineFragment(activeEditor, resource.fragment);
return true;
}
return false;
}
function getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
@@ -112,64 +70,3 @@ function getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
}
}
async function tryRevealLineInNotebook(tocProvider: MdTableOfContentsProvider, editor: vscode.NotebookEditor, fragment: string): Promise<boolean> {
const toc = await tocProvider.createForNotebook(editor.notebook);
const entry = toc.lookup(fragment);
if (!entry) {
return false;
}
const cell = editor.notebook.getCells().find(cell => cell.document.uri.toString() === entry.sectionLocation.uri.toString());
if (!cell) {
return false;
}
const range = new vscode.NotebookRange(cell.index, cell.index);
editor.selection = range;
editor.revealRange(range);
return true;
}
async function tryRevealLineUsingTocFragment(tocProvider: MdTableOfContentsProvider, editor: vscode.TextEditor, fragment: string): Promise<boolean> {
const toc = await tocProvider.getForDocument(editor.document);
const entry = toc.lookup(fragment);
if (entry) {
const lineStart = new vscode.Range(entry.line, 0, entry.line, 0);
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
return true;
}
return false;
}
function tryRevealLineUsingLineFragment(editor: vscode.TextEditor, fragment: string): boolean {
const lineNumberFragment = fragment.match(/^L(\d+)$/i);
if (lineNumberFragment) {
const line = +lineNumberFragment[1] - 1;
if (!isNaN(line)) {
const lineStart = new vscode.Range(line, 0, line, 0);
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
return true;
}
}
return false;
}
export async function resolveUriToMarkdownFile(workspace: IMdWorkspace, resource: vscode.Uri): Promise<ITextDocument | undefined> {
try {
const doc = await workspace.getOrLoadMarkdownDocument(resource);
if (doc) {
return doc;
}
} catch {
// Noop
}
// If no extension, try with `.md` extension
if (uri.Utils.extname(resource) === '') {
return workspace.getOrLoadMarkdownDocument(resource.with({ path: resource.path + '.md' }));
}
return undefined;
}

View File

@@ -11,53 +11,53 @@ const defaultResourceToKey = (resource: vscode.Uri): string => resource.toString
export class ResourceMap<T> {
private readonly map = new Map<string, { readonly uri: vscode.Uri; readonly value: T }>();
private readonly _map = new Map<string, { readonly uri: vscode.Uri; readonly value: T }>();
private readonly toKey: ResourceToKey;
private readonly _toKey: ResourceToKey;
constructor(toKey: ResourceToKey = defaultResourceToKey) {
this.toKey = toKey;
this._toKey = toKey;
}
public set(uri: vscode.Uri, value: T): this {
this.map.set(this.toKey(uri), { uri, value });
this._map.set(this._toKey(uri), { uri, value });
return this;
}
public get(resource: vscode.Uri): T | undefined {
return this.map.get(this.toKey(resource))?.value;
return this._map.get(this._toKey(resource))?.value;
}
public has(resource: vscode.Uri): boolean {
return this.map.has(this.toKey(resource));
return this._map.has(this._toKey(resource));
}
public get size(): number {
return this.map.size;
return this._map.size;
}
public clear(): void {
this.map.clear();
this._map.clear();
}
public delete(resource: vscode.Uri): boolean {
return this.map.delete(this.toKey(resource));
return this._map.delete(this._toKey(resource));
}
public *values(): IterableIterator<T> {
for (const entry of this.map.values()) {
for (const entry of this._map.values()) {
yield entry.value;
}
}
public *keys(): IterableIterator<vscode.Uri> {
for (const entry of this.map.values()) {
for (const entry of this._map.values()) {
yield entry.uri;
}
}
public *entries(): IterableIterator<[vscode.Uri, T]> {
for (const entry of this.map.values()) {
for (const entry of this._map.values()) {
yield [entry.uri, entry.value];
}
}

View File

@@ -1,116 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { ITextDocument } from '../types/textDocument';
import { IMdWorkspace } from '../workspace';
import { Disposable } from './dispose';
import { Lazy, lazy } from './lazy';
import { ResourceMap } from './resourceMap';
class LazyResourceMap<T> {
private readonly _map = new ResourceMap<Lazy<Promise<T>>>();
public has(resource: vscode.Uri): boolean {
return this._map.has(resource);
}
public get(resource: vscode.Uri): Promise<T> | undefined {
return this._map.get(resource)?.value;
}
public set(resource: vscode.Uri, value: Lazy<Promise<T>>) {
this._map.set(resource, value);
}
public delete(resource: vscode.Uri) {
this._map.delete(resource);
}
public entries(): Promise<Array<[vscode.Uri, T]>> {
return Promise.all(Array.from(this._map.entries(), async ([key, entry]) => {
return [key, await entry.value] as [vscode.Uri, T]; // {{SQL CARBON EDIT}} lewissanchez - Added strict typing
}));
}
}
/**
* Cache of information per-document in the workspace.
*
* The values are computed lazily and invalidated when the document changes.
*/
export class MdDocumentInfoCache<T> extends Disposable {
private readonly _cache = new LazyResourceMap<T>();
private readonly _loadingDocuments = new ResourceMap<Promise<ITextDocument | undefined>>();
public constructor(
private readonly workspace: IMdWorkspace,
private readonly getValue: (document: ITextDocument) => Promise<T>,
) {
super();
this._register(this.workspace.onDidChangeMarkdownDocument(doc => this.invalidate(doc)));
this._register(this.workspace.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this));
}
public async get(resource: vscode.Uri): Promise<T | undefined> {
let existing = this._cache.get(resource);
if (existing) {
return existing;
}
const doc = await this.loadDocument(resource);
if (!doc) {
return undefined;
}
// Check if we have invalidated
existing = this._cache.get(resource);
if (existing) {
return existing;
}
return this.resetEntry(doc)?.value;
}
public async getForDocument(document: ITextDocument): Promise<T> {
const existing = this._cache.get(document.uri);
if (existing) {
return existing;
}
return this.resetEntry(document).value;
}
private loadDocument(resource: vscode.Uri): Promise<ITextDocument | undefined> {
const existing = this._loadingDocuments.get(resource);
if (existing) {
return existing;
}
const p = this.workspace.getOrLoadMarkdownDocument(resource);
this._loadingDocuments.set(resource, p);
p.finally(() => {
this._loadingDocuments.delete(resource);
});
return p;
}
private resetEntry(document: ITextDocument): Lazy<Promise<T>> {
const value = lazy(() => this.getValue(document));
this._cache.set(document.uri, value);
return value;
}
private invalidate(document: ITextDocument): void {
if (this._cache.has(document.uri)) {
this.resetEntry(document);
}
}
private onDidDeleteDocument(resource: vscode.Uri) {
this._cache.delete(resource);
}
}

View File

@@ -1,198 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 { ITextDocument } from './types/textDocument';
import { coalesce } from './util/arrays';
import { Disposable } from './util/dispose';
import { isMarkdownFile, looksLikeMarkdownPath } from './util/file';
import { InMemoryDocument } from './util/inMemoryDocument';
import { Limiter } from './util/limiter';
import { ResourceMap } from './util/resourceMap';
/**
* Provides set of markdown files in the current workspace.
*/
export interface IMdWorkspace {
/**
* Get list of all known markdown files.
*/
getAllMarkdownDocuments(): Promise<Iterable<ITextDocument>>;
/**
* Check if a document already exists in the workspace contents.
*/
hasMarkdownDocument(resource: vscode.Uri): boolean;
getOrLoadMarkdownDocument(resource: vscode.Uri): Promise<ITextDocument | undefined>;
pathExists(resource: vscode.Uri): Promise<boolean>;
readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]>;
readonly onDidChangeMarkdownDocument: vscode.Event<ITextDocument>;
readonly onDidCreateMarkdownDocument: vscode.Event<ITextDocument>;
readonly onDidDeleteMarkdownDocument: vscode.Event<vscode.Uri>;
}
/**
* Provides set of markdown files known to VS Code.
*
* This includes both opened text documents and markdown files in the workspace.
*/
export class VsCodeMdWorkspace extends Disposable implements IMdWorkspace {
private readonly _onDidChangeMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<ITextDocument>());
private readonly _onDidCreateMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<ITextDocument>());
private readonly _onDidDeleteMarkdownDocumentEmitter = this._register(new vscode.EventEmitter<vscode.Uri>());
private _watcher: vscode.FileSystemWatcher | undefined;
private readonly _documentCache = new ResourceMap<ITextDocument>();
private readonly utf8Decoder = new TextDecoder('utf-8');
/**
* Reads and parses all .md documents in the workspace.
* Files are processed in batches, to keep the number of open files small.
*
* @returns Array of processed .md files.
*/
async getAllMarkdownDocuments(): Promise<ITextDocument[]> {
const maxConcurrent = 20;
const foundFiles = new ResourceMap<void>();
const limiter = new Limiter<ITextDocument | undefined>(maxConcurrent);
// Add files on disk
const resources = await vscode.workspace.findFiles('**/*.md', '**/node_modules/**');
const onDiskResults = await Promise.all(resources.map(resource => {
return limiter.queue(async () => {
const doc = await this.getOrLoadMarkdownDocument(resource);
if (doc) {
foundFiles.set(resource);
}
return doc;
});
}));
// Add opened files (such as untitled files)
const openTextDocumentResults = await Promise.all(vscode.workspace.textDocuments
.filter(doc => !foundFiles.has(doc.uri) && this.isRelevantMarkdownDocument(doc)));
return coalesce([...onDiskResults, ...openTextDocumentResults]);
}
public get onDidChangeMarkdownDocument() {
this.ensureWatcher();
return this._onDidChangeMarkdownDocumentEmitter.event;
}
public get onDidCreateMarkdownDocument() {
this.ensureWatcher();
return this._onDidCreateMarkdownDocumentEmitter.event;
}
public get onDidDeleteMarkdownDocument() {
this.ensureWatcher();
return this._onDidDeleteMarkdownDocumentEmitter.event;
}
private ensureWatcher(): void {
if (this._watcher) {
return;
}
this._watcher = this._register(vscode.workspace.createFileSystemWatcher('**/*.md'));
this._register(this._watcher.onDidChange(async resource => {
this._documentCache.delete(resource);
const document = await this.getOrLoadMarkdownDocument(resource);
if (document) {
this._onDidChangeMarkdownDocumentEmitter.fire(document);
}
}));
this._register(this._watcher.onDidCreate(async resource => {
const document = await this.getOrLoadMarkdownDocument(resource);
if (document) {
this._onDidCreateMarkdownDocumentEmitter.fire(document);
}
}));
this._register(this._watcher.onDidDelete(resource => {
this._documentCache.delete(resource);
this._onDidDeleteMarkdownDocumentEmitter.fire(resource);
}));
this._register(vscode.workspace.onDidOpenTextDocument(e => {
this._documentCache.delete(e.uri);
if (this.isRelevantMarkdownDocument(e)) {
this._onDidCreateMarkdownDocumentEmitter.fire(e);
}
}));
this._register(vscode.workspace.onDidChangeTextDocument(e => {
if (this.isRelevantMarkdownDocument(e.document)) {
this._onDidChangeMarkdownDocumentEmitter.fire(e.document);
}
}));
this._register(vscode.workspace.onDidCloseTextDocument(e => {
this._documentCache.delete(e.uri);
}));
}
private isRelevantMarkdownDocument(doc: vscode.TextDocument) {
return isMarkdownFile(doc) && doc.uri.scheme !== 'vscode-bulkeditpreview';
}
public async getOrLoadMarkdownDocument(resource: vscode.Uri): Promise<ITextDocument | undefined> {
const existing = this._documentCache.get(resource);
if (existing) {
return existing;
}
const matchingDocument = vscode.workspace.textDocuments.find((doc) => this.isRelevantMarkdownDocument(doc) && doc.uri.toString() === resource.toString());
if (matchingDocument) {
this._documentCache.set(resource, matchingDocument);
return matchingDocument;
}
if (!looksLikeMarkdownPath(resource)) {
return undefined;
}
try {
const bytes = await vscode.workspace.fs.readFile(resource);
// We assume that markdown is in UTF-8
const text = this.utf8Decoder.decode(bytes);
const doc = new InMemoryDocument(resource, text, 0);
this._documentCache.set(resource, doc);
return doc;
} catch {
return undefined;
}
}
public hasMarkdownDocument(resolvedHrefPath: vscode.Uri): boolean {
return this._documentCache.has(resolvedHrefPath);
}
public async pathExists(target: vscode.Uri): Promise<boolean> {
let targetResourceStat: vscode.FileStat | undefined;
try {
targetResourceStat = await vscode.workspace.fs.stat(target);
} catch {
return false;
}
return targetResourceStat.type === vscode.FileType.File || targetResourceStat.type === vscode.FileType.Directory;
}
public async readDirectory(resource: vscode.Uri): Promise<[string, vscode.FileType][]> {
return vscode.workspace.fs.readDirectory(resource);
}
}