deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/preview/.eslintrc.js
Normal file
10
packages/preview/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
46
packages/preview/README.md
Normal file
46
packages/preview/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - PREVIEW EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/preview` extension adds the ability to display rendered previews of supported resources.\
|
||||
The extension comes with built-in support for rendering `markdown` files.
|
||||
|
||||
## Contribute Custom Previews
|
||||
|
||||
To provide custom previews implement and bind the `PreviewHandler` interface, e.g.
|
||||
|
||||
```typescript
|
||||
@injectable
|
||||
class MyPreviewHandler implements PreviewHandler {
|
||||
...
|
||||
}
|
||||
// in container
|
||||
bind(MyPreviewHandler).toSelf().inSingletonScope();
|
||||
bind(PreviewHandler).toService(MyPreviewHandler);
|
||||
```
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [API documentation for `@theia/preview`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_preview.html)
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
55
packages/preview/package.json
Normal file
55
packages/preview/package.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@theia/preview",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia - Preview Extension",
|
||||
"dependencies": {
|
||||
"@theia/core": "1.68.0",
|
||||
"@theia/editor": "1.68.0",
|
||||
"@theia/mini-browser": "1.68.0",
|
||||
"@theia/monaco": "1.68.0",
|
||||
"@types/highlight.js": "^10.1.0",
|
||||
"highlight.js": "10.4.1",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"frontend": "lib/browser/preview-frontend-module",
|
||||
"secondaryWindow": "lib/browser/preview-frontend-module",
|
||||
"backend": "lib/node/preview-backend-module"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"theia-extension"
|
||||
],
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0"
|
||||
},
|
||||
"nyc": {
|
||||
"extends": "../../configs/nyc.json"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
19
packages/preview/src/browser/index.ts
Normal file
19
packages/preview/src/browser/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export * from './preview-uri';
|
||||
export * from './preview-handler';
|
||||
export { PreviewOpenerOptions } from './preview-contribution';
|
||||
17
packages/preview/src/browser/markdown/index.ts
Normal file
17
packages/preview/src/browser/markdown/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export * from './markdown-preview-handler';
|
||||
@@ -0,0 +1,228 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable no-unsanitized/property */
|
||||
|
||||
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
||||
|
||||
let disableJSDOM = enableJSDOM();
|
||||
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
FrontendApplicationConfigProvider.set({});
|
||||
|
||||
import * as chai from 'chai';
|
||||
import { expect } from 'chai';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { MarkdownPreviewHandler } from './markdown-preview-handler';
|
||||
|
||||
disableJSDOM();
|
||||
|
||||
chai.use(require('chai-string'));
|
||||
|
||||
let previewHandler: MarkdownPreviewHandler;
|
||||
|
||||
before(() => {
|
||||
previewHandler = new MarkdownPreviewHandler();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(previewHandler as any).linkNormalizer = {
|
||||
normalizeLink: (documentUri: URI, link: string) =>
|
||||
'endpoint/' + documentUri.parent.resolve(link).path.toString().substring(1)
|
||||
};
|
||||
});
|
||||
|
||||
describe('markdown-preview-handler', () => {
|
||||
|
||||
before(() => {
|
||||
disableJSDOM = enableJSDOM();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
disableJSDOM();
|
||||
});
|
||||
|
||||
it('renders html with line information', async () => {
|
||||
await assertRenderedContent(exampleMarkdown1, exampleHtml1);
|
||||
});
|
||||
|
||||
it('renders images', async () => {
|
||||
await assertRenderedContent(exampleMarkdown2, exampleHtml2);
|
||||
});
|
||||
|
||||
it('renders HTML image as block', async () => {
|
||||
await assertRenderedContent(exampleMarkdown3, exampleHtml3);
|
||||
});
|
||||
|
||||
it('renders HTML images inlined', async () => {
|
||||
await assertRenderedContent(exampleMarkdown4, exampleHtml4);
|
||||
});
|
||||
|
||||
it('renders multiple HTML images in a html block', async () => {
|
||||
await assertRenderedContent(exampleMarkdown5, exampleHtml5);
|
||||
});
|
||||
|
||||
it('finds element for source line', () => {
|
||||
document.body.innerHTML = exampleHtml1;
|
||||
const element = previewHandler.findElementForSourceLine(document.body, 4);
|
||||
expect(element).not.to.be.equal(undefined);
|
||||
expect(element!.tagName).to.be.equal('H2');
|
||||
expect(element!.textContent).to.be.equal('License');
|
||||
});
|
||||
|
||||
it('finds previous element for empty source line', () => {
|
||||
document.body.innerHTML = exampleHtml1;
|
||||
const element = previewHandler.findElementForSourceLine(document.body, 3);
|
||||
expect(element).not.to.be.equal(undefined);
|
||||
expect(element!.tagName).to.be.equal('P');
|
||||
expect(element!.textContent).that.startWith('Shows a preview of supported resources.');
|
||||
});
|
||||
|
||||
it('finds source line for offset in html', () => {
|
||||
mockOffsetProperties();
|
||||
document.body.innerHTML = exampleHtml1;
|
||||
for (const expectedLine of [0, 1, 4, 5]) {
|
||||
const line = previewHandler.getSourceLineForOffset(document.body, offsetForLine(expectedLine));
|
||||
expect(line).to.be.equal(expectedLine);
|
||||
}
|
||||
});
|
||||
|
||||
it('interpolates source lines for offset in html', () => {
|
||||
mockOffsetProperties();
|
||||
document.body.innerHTML = exampleHtml1;
|
||||
const expectedLines = [1, 2, 3, 4];
|
||||
const offsets = expectedLines.map(l => offsetForLine(l));
|
||||
for (let i = 0; i < expectedLines.length; i++) {
|
||||
const expectedLine = expectedLines[i];
|
||||
const offset = offsets[i];
|
||||
const line = previewHandler.getSourceLineForOffset(document.body, offset);
|
||||
expect(line).to.be.equal(expectedLine);
|
||||
}
|
||||
});
|
||||
|
||||
it('can handle \'.md\' files', () => {
|
||||
expect(previewHandler.canHandle(new URI('a.md'))).greaterThan(0);
|
||||
});
|
||||
|
||||
it('can handle \'.markdown\' files', () => {
|
||||
expect(previewHandler.canHandle(new URI('a.markdown'))).greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
async function assertRenderedContent(source: string, expectation: string): Promise<void> {
|
||||
const contentElement = previewHandler.renderContent({ content: source, originUri: new URI('file:///workspace/DEMO.md') });
|
||||
expect(contentElement.innerHTML).equals(expectation);
|
||||
}
|
||||
|
||||
const exampleMarkdown1 = //
|
||||
`# Theia - Preview Extension
|
||||
Shows a preview of supported resources.
|
||||
See [here](https://github.com/eclipse-theia/theia).
|
||||
|
||||
## License
|
||||
[Apache-2.0](https://github.com/eclipse-theia/theia/blob/master/LICENSE)
|
||||
`;
|
||||
|
||||
const exampleHtml1 = //
|
||||
`<h1 id="theia---preview-extension" tabindex="-1" class="line" data-line="0">Theia - Preview Extension</h1>
|
||||
<p class="line" data-line="1">Shows a preview of supported resources.
|
||||
See <a href="https://github.com/eclipse-theia/theia">here</a>.</p>
|
||||
<h2 id="license" tabindex="-1" class="line" data-line="4">License</h2>
|
||||
<p class="line" data-line="5"><a href="https://github.com/eclipse-theia/theia/blob/master/LICENSE">Apache-2.0</a></p>
|
||||
`;
|
||||
|
||||
const exampleMarkdown2 = //
|
||||
`# Heading
|
||||

|
||||
`;
|
||||
|
||||
const exampleHtml2 = //
|
||||
`<h1 id="heading" tabindex="-1" class="line" data-line="0">Heading</h1>
|
||||
<p class="line" data-line="1"><img src="endpoint/workspace/subfolder/image.png" alt="alternativetext"></p>
|
||||
`;
|
||||
|
||||
const exampleMarkdown3 = //
|
||||
`# Block HTML Image
|
||||
<img src="subfolder/image1.png" alt="tada"/>
|
||||
|
||||
# Block HTML Image
|
||||
<img src="subfolder/image3.png" alt="tada"/>
|
||||
`;
|
||||
|
||||
const exampleHtml3 = //
|
||||
`<h1 id="block-html-image" tabindex="-1" class="line" data-line="0">Block HTML Image</h1>
|
||||
<img src="endpoint/workspace/subfolder/image1.png" alt="tada">
|
||||
<h1 id="block-html-image-1" tabindex="-1" class="line" data-line="3">Block HTML Image</h1>
|
||||
<img src="endpoint/workspace/subfolder/image3.png" alt="tada">
|
||||
`;
|
||||
|
||||
const exampleMarkdown4 = //
|
||||
`# Inlined HTML Image
|
||||
text in paragraph <img src="subfolder/image2.png" alt="tada"/>
|
||||
`;
|
||||
|
||||
const exampleHtml4 = //
|
||||
`<h1 id="inlined-html-image" tabindex="-1" class="line" data-line="0">Inlined HTML Image</h1>
|
||||
<p class="line" data-line="1">text in paragraph <img src="endpoint/workspace/subfolder/image2.png" alt="tada"></p>
|
||||
`;
|
||||
|
||||
const exampleMarkdown5 = //
|
||||
`# Multiple HTML Images nested in blocks
|
||||
word <p>
|
||||
<img src="subfolder/image2.png" alt="tada"/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<img src="subfolder/image2.png" alt="tada"/>
|
||||
</p>
|
||||
`;
|
||||
|
||||
const exampleHtml5 = //
|
||||
`<h1 id="multiple-html-images-nested-in-blocks" tabindex="-1" class="line" data-line="0">Multiple HTML Images nested in blocks</h1>
|
||||
<p class="line" data-line="1">word </p><p>
|
||||
<img src="endpoint/workspace/subfolder/image2.png" alt="tada"></p>
|
||||
<p></p>
|
||||
<p>
|
||||
<img src="endpoint/workspace/subfolder/image2.png" alt="tada">
|
||||
</p>
|
||||
`;
|
||||
|
||||
/**
|
||||
* `offsetTop` of elements to be `sourceLine` number times `20`.
|
||||
*/
|
||||
function mockOffsetProperties(): void {
|
||||
Object.defineProperties(HTMLElement.prototype, {
|
||||
offsetLeft: {
|
||||
get: () => 0
|
||||
},
|
||||
offsetTop: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
get: function (): any {
|
||||
const element = this as HTMLElement;
|
||||
const line = Number.parseInt(element.getAttribute('data-line') || '0');
|
||||
return offsetForLine(line);
|
||||
}
|
||||
},
|
||||
offsetHeight: {
|
||||
get: () => 0
|
||||
},
|
||||
offsetWidth: {
|
||||
get: () => 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function offsetForLine(line: number): number {
|
||||
return line * 20;
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { OpenerService } from '@theia/core/lib/browser';
|
||||
import { isOSX } from '@theia/core/lib/common';
|
||||
import { Path } from '@theia/core/lib/common/path';
|
||||
|
||||
import * as hljs from 'highlight.js';
|
||||
import * as markdownit from '@theia/core/shared/markdown-it';
|
||||
import * as markdownitemoji from '@theia/core/shared/markdown-it-emoji';
|
||||
import * as anchor from '@theia/core/shared/markdown-it-anchor';
|
||||
import * as DOMPurify from '@theia/core/shared/dompurify';
|
||||
import { PreviewUri } from '../preview-uri';
|
||||
import { PreviewHandler, RenderContentParams } from '../preview-handler';
|
||||
import { PreviewOpenerOptions } from '../preview-contribution';
|
||||
import { PreviewLinkNormalizer } from '../preview-link-normalizer';
|
||||
|
||||
@injectable()
|
||||
export class MarkdownPreviewHandler implements PreviewHandler {
|
||||
|
||||
readonly iconClass: string = 'markdown-icon file-icon';
|
||||
readonly contentClass: string = 'markdown-preview';
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
|
||||
@inject(PreviewLinkNormalizer)
|
||||
protected readonly linkNormalizer: PreviewLinkNormalizer;
|
||||
|
||||
canHandle(uri: URI): number {
|
||||
return uri.scheme === 'file'
|
||||
&& (
|
||||
uri.path.ext.toLowerCase() === '.md' ||
|
||||
uri.path.ext.toLowerCase() === '.markdown'
|
||||
) ? 500 : 0;
|
||||
}
|
||||
|
||||
renderContent(params: RenderContentParams): HTMLElement {
|
||||
const content = params.content;
|
||||
const renderedContent = this.getEngine().render(content, params);
|
||||
const contentElement = document.createElement('div');
|
||||
contentElement.classList.add(this.contentClass);
|
||||
contentElement.innerHTML = DOMPurify.sanitize(renderedContent);
|
||||
this.addLinkClickedListener(contentElement, params);
|
||||
return contentElement;
|
||||
}
|
||||
|
||||
protected addLinkClickedListener(contentElement: HTMLElement, params: RenderContentParams): void {
|
||||
contentElement.addEventListener('click', (event: MouseEvent) => {
|
||||
const candidate = (event.target || event.srcElement) as HTMLElement;
|
||||
const link = this.findLink(candidate, contentElement);
|
||||
if (link) {
|
||||
event.preventDefault();
|
||||
if (link.startsWith('#')) {
|
||||
this.revealFragment(contentElement, link);
|
||||
} else {
|
||||
const preview = !(isOSX ? event.metaKey : event.ctrlKey);
|
||||
const uri = this.resolveUri(link, params.originUri, preview);
|
||||
this.openLink(uri, params.originUri);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected findLink(element: HTMLElement, container: HTMLElement): string | undefined {
|
||||
let candidate = element;
|
||||
while (candidate.tagName !== 'A') {
|
||||
if (candidate === container) {
|
||||
return;
|
||||
}
|
||||
candidate = candidate.parentElement!;
|
||||
if (!candidate) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return candidate.getAttribute('href') || undefined;
|
||||
}
|
||||
|
||||
protected async openLink(uri: URI, originUri: URI): Promise<void> {
|
||||
const opener = await this.openerService.getOpener(uri);
|
||||
opener.open(uri, <PreviewOpenerOptions>{ originUri });
|
||||
}
|
||||
|
||||
protected resolveUri(link: string, uri: URI, preview: boolean): URI {
|
||||
const linkURI = new URI(link);
|
||||
// URIs are always absolute, check link as a path whether it is relative
|
||||
if (!new Path(link).isAbsolute && linkURI.scheme === uri.scheme &&
|
||||
(!linkURI.authority || linkURI.authority === uri.authority)) {
|
||||
// get a relative path from URI by trimming leading `/`
|
||||
const relativePath = linkURI.path.toString().substring(1);
|
||||
const resolvedUri = uri.parent.resolve(relativePath).withFragment(linkURI.fragment).withQuery(linkURI.query);
|
||||
return preview ? PreviewUri.encode(resolvedUri) : resolvedUri;
|
||||
}
|
||||
return linkURI;
|
||||
}
|
||||
|
||||
protected revealFragment(contentElement: HTMLElement, fragment: string): void {
|
||||
const elementToReveal = this.findElementForFragment(contentElement, fragment);
|
||||
if (!elementToReveal) {
|
||||
return;
|
||||
}
|
||||
elementToReveal.scrollIntoView();
|
||||
}
|
||||
|
||||
findElementForFragment(content: HTMLElement, link: string): HTMLElement | undefined {
|
||||
const fragment = link.startsWith('#') ? link.substring(1) : link;
|
||||
const filter: NodeFilter = {
|
||||
acceptNode: (node: Node) => {
|
||||
if (node instanceof HTMLHeadingElement) {
|
||||
if (node.tagName.toLowerCase().startsWith('h') && node.id === fragment) {
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
return NodeFilter.FILTER_SKIP;
|
||||
}
|
||||
return NodeFilter.FILTER_SKIP;
|
||||
}
|
||||
};
|
||||
const treeWalker = document.createTreeWalker(content, NodeFilter.SHOW_ELEMENT, filter);
|
||||
if (treeWalker.nextNode()) {
|
||||
const element = treeWalker.currentNode as HTMLElement;
|
||||
return element;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
findElementForSourceLine(content: HTMLElement, sourceLine: number): HTMLElement | undefined {
|
||||
const markedElements = content.getElementsByClassName('line');
|
||||
let matchedElement: HTMLElement | undefined;
|
||||
for (let i = 0; i < markedElements.length; i++) {
|
||||
const element = markedElements[i];
|
||||
const line = Number.parseInt(element.getAttribute('data-line') || '0');
|
||||
if (line > sourceLine) {
|
||||
break;
|
||||
}
|
||||
matchedElement = element as HTMLElement;
|
||||
}
|
||||
return matchedElement;
|
||||
}
|
||||
|
||||
getSourceLineForOffset(content: HTMLElement, offset: number): number | undefined {
|
||||
const lineElements = this.getLineElementsAtOffset(content, offset);
|
||||
if (lineElements.length < 1) {
|
||||
return undefined;
|
||||
}
|
||||
const firstLineNumber = this.getLineNumberFromAttribute(lineElements[0]);
|
||||
if (firstLineNumber === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (lineElements.length === 1) {
|
||||
return firstLineNumber;
|
||||
}
|
||||
const secondLineNumber = this.getLineNumberFromAttribute(lineElements[1]);
|
||||
if (secondLineNumber === undefined) {
|
||||
return firstLineNumber;
|
||||
}
|
||||
const y1 = lineElements[0].offsetTop;
|
||||
const y2 = lineElements[1].offsetTop;
|
||||
const dY = (offset - y1) / (y2 - y1);
|
||||
const dL = (secondLineNumber - firstLineNumber) * dY;
|
||||
const line = firstLineNumber + Math.floor(dL);
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns two significant line elements for the given offset.
|
||||
*/
|
||||
protected getLineElementsAtOffset(content: HTMLElement, offset: number): HTMLElement[] {
|
||||
let skipNext = false;
|
||||
const filter: NodeFilter = {
|
||||
acceptNode: (node: Node) => {
|
||||
if (node instanceof HTMLElement) {
|
||||
if (node.classList.contains('line')) {
|
||||
if (skipNext) {
|
||||
return NodeFilter.FILTER_SKIP;
|
||||
}
|
||||
if (node.offsetTop > offset) {
|
||||
skipNext = true;
|
||||
}
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
return NodeFilter.FILTER_SKIP;
|
||||
}
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
};
|
||||
const treeWalker = document.createTreeWalker(content, NodeFilter.SHOW_ELEMENT, filter);
|
||||
const lineElements: HTMLElement[] = [];
|
||||
while (treeWalker.nextNode()) {
|
||||
const element = treeWalker.currentNode as HTMLElement;
|
||||
lineElements.push(element);
|
||||
}
|
||||
return lineElements.slice(-2);
|
||||
}
|
||||
|
||||
protected getLineNumberFromAttribute(element: HTMLElement): number | undefined {
|
||||
const attribute = element.getAttribute('data-line');
|
||||
return attribute ? Number.parseInt(attribute) : undefined;
|
||||
}
|
||||
|
||||
protected engine: markdownit | undefined;
|
||||
protected getEngine(): markdownit {
|
||||
if (!this.engine) {
|
||||
const engine: markdownit = this.engine = markdownit({
|
||||
html: true,
|
||||
linkify: true,
|
||||
highlight: (str, lang) => {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
return '<pre class="hljs"><code><div>' + hljs.highlight(lang, str, true).value + '</div></code></pre>';
|
||||
} catch { }
|
||||
}
|
||||
return '<pre class="hljs"><code><div>' + engine.utils.escapeHtml(str) + '</div></code></pre>';
|
||||
}
|
||||
}).use(markdownitemoji.full).use(anchor.default, {});
|
||||
const renderers = ['heading_open', 'paragraph_open', 'list_item_open', 'blockquote_open', 'code_block', 'image', 'fence'];
|
||||
for (const renderer of renderers) {
|
||||
const originalRenderer = engine.renderer.rules[renderer];
|
||||
engine.renderer.rules[renderer] = (tokens, index, options, env, self) => {
|
||||
const token = tokens[index];
|
||||
if (token.map) {
|
||||
const line = token.map[0];
|
||||
token.attrJoin('class', 'line');
|
||||
token.attrSet('data-line', line.toString());
|
||||
}
|
||||
return (originalRenderer)
|
||||
// tslint:disable-next-line:no-void-expression
|
||||
? originalRenderer(tokens, index, options, env, self)
|
||||
: self.renderToken(tokens, index, options);
|
||||
};
|
||||
}
|
||||
const originalImageRenderer = engine.renderer.rules.image;
|
||||
if (originalImageRenderer) {
|
||||
engine.renderer.rules.image = (tokens, index, options, env, self) => {
|
||||
if (RenderContentParams.is(env)) {
|
||||
const documentUri = env.originUri;
|
||||
const token = tokens[index];
|
||||
if (token.attrs) {
|
||||
const srcAttr = token.attrs.find(a => a[0] === 'src');
|
||||
if (srcAttr) {
|
||||
const href = srcAttr[1];
|
||||
srcAttr[1] = this.linkNormalizer.normalizeLink(documentUri, href);
|
||||
}
|
||||
}
|
||||
}
|
||||
return originalImageRenderer(tokens, index, options, env, self);
|
||||
};
|
||||
}
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
const parseDOM = (html: string) =>
|
||||
domParser.parseFromString(html, 'text/html').getElementsByTagName('body')[0] as HTMLElement;
|
||||
|
||||
const modifyDOM = (body: HTMLElement, tag: string, procedure: (element: Element) => void) => {
|
||||
const elements = body.getElementsByTagName(tag);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const element = elements.item(i);
|
||||
if (element) {
|
||||
procedure(element);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeAllImgSrcInHTML = (html: string, normalizeLink: (link: string) => string) => {
|
||||
const body = parseDOM(html);
|
||||
modifyDOM(body, 'img', img => {
|
||||
const src = img.getAttributeNode('src');
|
||||
if (src) {
|
||||
src.nodeValue = normalizeLink(src.nodeValue || '');
|
||||
}
|
||||
});
|
||||
return body.innerHTML;
|
||||
};
|
||||
|
||||
for (const name of ['html_block', 'html_inline']) {
|
||||
const originalRenderer = engine.renderer.rules[name];
|
||||
if (originalRenderer) {
|
||||
engine.renderer.rules[name] = (tokens, index, options, env, self) => {
|
||||
const currentToken = tokens[index];
|
||||
const content = currentToken.content;
|
||||
if (content.includes('<img') && RenderContentParams.is(env)) {
|
||||
const documentUri = env.originUri;
|
||||
currentToken.content = normalizeAllImgSrcInHTML(content, link => this.linkNormalizer.normalizeLink(documentUri, link));
|
||||
}
|
||||
return originalRenderer(tokens, index, options, env, self);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.engine;
|
||||
}
|
||||
|
||||
}
|
||||
18
packages/preview/src/browser/markdown/style/index.css
Normal file
18
packages/preview/src/browser/markdown/style/index.css
Normal file
@@ -0,0 +1,18 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2018 TypeFox and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
@import "./markdown.css";
|
||||
@import "./tomorrow.css";
|
||||
203
packages/preview/src/browser/markdown/style/markdown.css
Normal file
203
packages/preview/src/browser/markdown/style/markdown.css
Normal file
@@ -0,0 +1,203 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2018 TypeFox and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.markdown-preview {
|
||||
font-family: var(--theia-ui-font-family);
|
||||
font-size: 14px;
|
||||
padding: 0 26px;
|
||||
line-height: var(--theia-content-line-height);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.markdown-preview:focus {
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.markdown-preview .line {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.markdown-preview .line:hover:before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -12px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.markdown-preview li.line:hover:before {
|
||||
left: -30px;
|
||||
}
|
||||
|
||||
.markdown-preview .line:hover:before {
|
||||
border-left: 3px solid var(--theia-editor-foreground);
|
||||
}
|
||||
|
||||
.markdown-preview .line .line:hover:before {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.markdown-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.markdown-preview a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-preview a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-preview a:focus,
|
||||
.markdown-preview input:focus,
|
||||
.markdown-preview select:focus,
|
||||
.markdown-preview textarea:focus {
|
||||
outline: 1px solid -webkit-focus-ring-color;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.markdown-preview hr {
|
||||
border: 0;
|
||||
height: 2px;
|
||||
border-bottom: 2px solid;
|
||||
}
|
||||
|
||||
.markdown-preview h1 {
|
||||
padding-bottom: 0.3em;
|
||||
line-height: 1.2;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
.markdown-preview h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.markdown-preview h1 code,
|
||||
.markdown-preview h2 code,
|
||||
.markdown-preview h3 code,
|
||||
.markdown-preview h4 code,
|
||||
.markdown-preview h5 code,
|
||||
.markdown-preview h6 code {
|
||||
font-size: inherit;
|
||||
line-height: auto;
|
||||
}
|
||||
|
||||
.markdown-preview table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.markdown-preview table > thead > tr > th {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid;
|
||||
border-color: rgba(255, 255, 255, 0.69);
|
||||
}
|
||||
|
||||
.theia-light .markdown-preview table > thead > tr > th {
|
||||
border-color: rgba(0, 0, 0, 0.69);
|
||||
}
|
||||
|
||||
.markdown-preview table > thead > tr > th,
|
||||
.markdown-preview table > thead > tr > td,
|
||||
.markdown-preview table > tbody > tr > th,
|
||||
.markdown-preview table > tbody > tr > td {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.markdown-preview table > tbody > tr + tr > td {
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
.markdown-preview blockquote {
|
||||
margin: 0 7px 0 5px;
|
||||
padding: 0 16px 0 10px;
|
||||
border-left: 5px solid;
|
||||
background: var(--theia-textBlockQuote-background);
|
||||
border-color: var(--theia-textBlockQuote-border);
|
||||
}
|
||||
|
||||
.markdown-preview code {
|
||||
font-family: var(--theia-code-font-family);
|
||||
font-size: var(--theia-code-font-size);
|
||||
line-height: var(--theia-code-line-height);
|
||||
color: var(--md-orange-800);
|
||||
}
|
||||
|
||||
.markdown-preview.wordWrap pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.markdown-preview pre:not(.hljs),
|
||||
.markdown-preview pre.hljs code > div {
|
||||
padding: 16px;
|
||||
border-radius: 3px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.markdown-preview,
|
||||
.markdown-preview pre code {
|
||||
color: var(--theia-editor-foreground);
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
/** Theming */
|
||||
|
||||
.theia-light .markdown-preview pre {
|
||||
background-color: rgba(220, 220, 220, 0.4);
|
||||
}
|
||||
|
||||
.theia-dark .markdown-preview pre {
|
||||
background-color: rgba(10, 10, 10, 0.4);
|
||||
}
|
||||
|
||||
.theia-high-contrast .markdown-preview pre {
|
||||
background-color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.vscode-high-contrast .markdown-preview h1 {
|
||||
border-color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.theia-light .markdown-preview table > thead > tr > th {
|
||||
border-color: rgba(0, 0, 0, 0.69);
|
||||
}
|
||||
|
||||
.theia-dark .markdown-preview table > thead > tr > th {
|
||||
border-color: rgba(255, 255, 255, 0.69);
|
||||
}
|
||||
|
||||
.theia-light .markdown-preview h1,
|
||||
.theia-light .markdown-preview hr,
|
||||
.theia-light .markdown-preview table > tbody > tr + tr > td {
|
||||
border-color: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.theia-dark .markdown-preview h1,
|
||||
.theia-dark .markdown-preview hr,
|
||||
.theia-dark .markdown-preview table > tbody > tr + tr > td {
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
105
packages/preview/src/browser/markdown/style/tomorrow.css
Normal file
105
packages/preview/src/browser/markdown/style/tomorrow.css
Normal file
@@ -0,0 +1,105 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2018 TypeFox and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
/* Tomorrow Theme */
|
||||
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||
|
||||
/* Original theme - Copyright (C) 2013 Chris Kempson http://chriskempson.com
|
||||
/* released under the MIT License */
|
||||
/* https://github.com/chriskempson/tomorrow-theme */
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Copied from https://github.com/microsoft/vscode/blob/08537497eecd3172390194693d3d7c0ec8f52b70/extensions/markdown-language-features/media/tomorrow.css
|
||||
* with modifications.
|
||||
*/
|
||||
|
||||
/* This theme is used to style the output of the highlight.js library which is
|
||||
* licensed under the BSD-3-Clause. See https://github.com/highlightjs/highlight.js/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
/* Tomorrow Comment */
|
||||
.theia-preview-widget .hljs-comment,
|
||||
.theia-preview-widget .hljs-quote {
|
||||
color: #8e908c;
|
||||
}
|
||||
|
||||
/* Tomorrow Red */
|
||||
.theia-preview-widget .hljs-variable,
|
||||
.theia-preview-widget .hljs-template-variable,
|
||||
.theia-preview-widget .hljs-tag,
|
||||
.theia-preview-widget .hljs-name,
|
||||
.theia-preview-widget .hljs-selector-id,
|
||||
.theia-preview-widget .hljs-selector-class,
|
||||
.theia-preview-widget .hljs-regexp,
|
||||
.theia-preview-widget .hljs-deletion {
|
||||
color: #c82829;
|
||||
}
|
||||
|
||||
/* Tomorrow Orange */
|
||||
.theia-preview-widget .hljs-number,
|
||||
.theia-preview-widget .hljs-built_in,
|
||||
.theia-preview-widget .hljs-builtin-name,
|
||||
.theia-preview-widget .hljs-literal,
|
||||
.theia-preview-widget .hljs-type,
|
||||
.theia-preview-widget .hljs-params,
|
||||
.theia-preview-widget .hljs-meta,
|
||||
.theia-preview-widget .hljs-link {
|
||||
color: #f5871f;
|
||||
}
|
||||
|
||||
/* Tomorrow Yellow */
|
||||
.theia-preview-widget .hljs-attribute {
|
||||
color: #eab700;
|
||||
}
|
||||
|
||||
/* Tomorrow Green */
|
||||
.theia-preview-widget .hljs-string,
|
||||
.theia-preview-widget .hljs-symbol,
|
||||
.theia-preview-widget .hljs-bullet,
|
||||
.theia-preview-widget .hljs-addition {
|
||||
color: #718c00;
|
||||
}
|
||||
|
||||
/* Tomorrow Blue */
|
||||
.theia-preview-widget .hljs-title,
|
||||
.theia-preview-widget .hljs-section {
|
||||
color: #4271ae;
|
||||
}
|
||||
|
||||
/* Tomorrow Purple */
|
||||
.theia-preview-widget .hljs-keyword,
|
||||
.theia-preview-widget .hljs-selector-tag {
|
||||
color: #8959a8;
|
||||
}
|
||||
|
||||
.theia-preview-widget .hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
color: #4d4d4c;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.theia-preview-widget .hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.theia-preview-widget .hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
279
packages/preview/src/browser/preview-contribution.ts
Normal file
279
packages/preview/src/browser/preview-contribution.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { Widget } from '@theia/core/shared/@lumino/widgets';
|
||||
import { FrontendApplicationContribution, WidgetOpenerOptions, NavigatableWidgetOpenHandler, codicon } from '@theia/core/lib/browser';
|
||||
import { EditorManager, TextEditor, EditorWidget, EditorContextMenu } from '@theia/editor/lib/browser';
|
||||
import { DisposableCollection, CommandContribution, CommandRegistry, Command, MenuContribution, MenuModelRegistry, Disposable } from '@theia/core/lib/common';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { MiniBrowserCommands } from '@theia/mini-browser/lib/browser/mini-browser-open-handler';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import { PreviewWidget } from './preview-widget';
|
||||
import { PreviewHandlerProvider } from './preview-handler';
|
||||
import { PreviewUri } from './preview-uri';
|
||||
import { PreviewPreferences } from '../common/preview-preferences';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
import debounce = require('@theia/core/shared/lodash.debounce');
|
||||
|
||||
export namespace PreviewCommands {
|
||||
/**
|
||||
* No `label`. Otherwise, it would show up in the `Command Palette` and we already have the `Preview` open handler.
|
||||
* See in (`WorkspaceCommandContribution`)[https://bit.ly/2DncrSD].
|
||||
*/
|
||||
export const OPEN = Command.toLocalizedCommand({
|
||||
id: 'preview:open',
|
||||
label: 'Open Preview',
|
||||
iconClass: codicon('open-preview')
|
||||
}, 'vscode.markdown-language-features/package/markdown.preview.title');
|
||||
export const OPEN_SOURCE: Command = {
|
||||
id: 'preview.open.source',
|
||||
iconClass: codicon('go-to-file')
|
||||
};
|
||||
}
|
||||
|
||||
export interface PreviewOpenerOptions extends WidgetOpenerOptions {
|
||||
originUri?: URI;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
// eslint-disable-next-line max-len
|
||||
export class PreviewContribution extends NavigatableWidgetOpenHandler<PreviewWidget> implements CommandContribution, MenuContribution, FrontendApplicationContribution, TabBarToolbarContribution {
|
||||
|
||||
readonly id = PreviewUri.id;
|
||||
readonly label = nls.localize(MiniBrowserCommands.PREVIEW_CATEGORY_KEY, MiniBrowserCommands.PREVIEW_CATEGORY);
|
||||
|
||||
@inject(EditorManager)
|
||||
protected readonly editorManager: EditorManager;
|
||||
|
||||
@inject(PreviewHandlerProvider)
|
||||
protected readonly previewHandlerProvider: PreviewHandlerProvider;
|
||||
|
||||
@inject(PreviewPreferences)
|
||||
protected readonly preferences: PreviewPreferences;
|
||||
|
||||
protected readonly synchronizedUris = new Set<string>();
|
||||
|
||||
protected scrollSyncLockOn: 'preview' | 'editor' | undefined = undefined;
|
||||
|
||||
protected scrollSyncLockTimeout: number | undefined;
|
||||
|
||||
onStart(): void {
|
||||
this.onCreated(previewWidget => {
|
||||
this.registerOpenOnDoubleClick(previewWidget);
|
||||
this.registerEditorAndPreviewSync(previewWidget);
|
||||
});
|
||||
this.editorManager.onCreated(editorWidget => {
|
||||
this.registerEditorAndPreviewSync(editorWidget);
|
||||
});
|
||||
}
|
||||
|
||||
protected async lockScrollSync(on: 'preview' | 'editor', delay: number = 50): Promise<void> {
|
||||
this.scrollSyncLockOn = on;
|
||||
if (this.scrollSyncLockTimeout) {
|
||||
window.clearTimeout(this.scrollSyncLockTimeout);
|
||||
}
|
||||
this.scrollSyncLockTimeout = window.setTimeout(() => {
|
||||
this.scrollSyncLockOn = undefined;
|
||||
}, delay);
|
||||
}
|
||||
|
||||
protected async registerEditorAndPreviewSync(source: PreviewWidget | EditorWidget): Promise<void> {
|
||||
let uri: string;
|
||||
let editorWidget: EditorWidget | undefined;
|
||||
let previewWidget: PreviewWidget | undefined;
|
||||
if (source instanceof EditorWidget) {
|
||||
editorWidget = source;
|
||||
uri = editorWidget.editor.uri.toString();
|
||||
previewWidget = await this.getWidget(editorWidget.editor.uri);
|
||||
} else {
|
||||
previewWidget = source;
|
||||
uri = previewWidget.getUri().toString();
|
||||
editorWidget = await this.editorManager.getByUri(previewWidget.getUri());
|
||||
}
|
||||
if (!previewWidget || !editorWidget || !uri) {
|
||||
return;
|
||||
}
|
||||
if (this.synchronizedUris.has(uri)) {
|
||||
return;
|
||||
}
|
||||
const syncDisposables = new DisposableCollection();
|
||||
previewWidget.disposed.connect(() => syncDisposables.dispose());
|
||||
editorWidget.disposed.connect(() => syncDisposables.dispose());
|
||||
|
||||
const editor = editorWidget.editor;
|
||||
syncDisposables.push(editor.onScrollChanged(debounce(() => {
|
||||
if (this.scrollSyncLockOn === 'editor') {
|
||||
// avoid recursive scroll synchronization
|
||||
return;
|
||||
}
|
||||
this.lockScrollSync('preview');
|
||||
const range = editor.getVisibleRanges();
|
||||
if (range.length > 0) {
|
||||
this.revealSourceLineInPreview(previewWidget!, range[0].start);
|
||||
}
|
||||
}), 100));
|
||||
syncDisposables.push(this.synchronizeScrollToEditor(previewWidget, editor));
|
||||
|
||||
this.synchronizedUris.add(uri);
|
||||
syncDisposables.push(Disposable.create(() => this.synchronizedUris.delete(uri)));
|
||||
}
|
||||
|
||||
protected revealSourceLineInPreview(previewWidget: PreviewWidget, position: Position): void {
|
||||
previewWidget.revealForSourceLine(position.line);
|
||||
}
|
||||
|
||||
protected synchronizeScrollToEditor(previewWidget: PreviewWidget, editor: TextEditor): Disposable {
|
||||
return previewWidget.onDidScroll(sourceLine => {
|
||||
if (this.scrollSyncLockOn === 'preview') {
|
||||
// avoid recursive scroll synchronization
|
||||
return;
|
||||
}
|
||||
const line = Math.floor(sourceLine);
|
||||
this.lockScrollSync('editor'); // avoid recursive scroll synchronization
|
||||
editor.revealRange({
|
||||
start: {
|
||||
line,
|
||||
character: 0
|
||||
},
|
||||
end: {
|
||||
line: line + 1,
|
||||
character: 0
|
||||
}
|
||||
}, { at: 'top' });
|
||||
});
|
||||
}
|
||||
|
||||
protected registerOpenOnDoubleClick(ref: PreviewWidget): void {
|
||||
const disposable = ref.onDidDoubleClick(async location => {
|
||||
const { editor } = await this.openSource(ref);
|
||||
editor.revealPosition(location.range.start);
|
||||
editor.selection = {
|
||||
...location.range,
|
||||
direction: 'ltr'
|
||||
};
|
||||
ref.revealForSourceLine(location.range.start.line);
|
||||
});
|
||||
ref.disposed.connect(() => disposable.dispose());
|
||||
}
|
||||
|
||||
canHandle(uri: URI): number {
|
||||
if (!this.previewHandlerProvider.canHandle(uri)) {
|
||||
return 0;
|
||||
}
|
||||
const editorPriority = this.editorManager.canHandle(uri);
|
||||
if (editorPriority === 0) {
|
||||
return 200;
|
||||
}
|
||||
if (PreviewUri.match(uri)) {
|
||||
return editorPriority * 2;
|
||||
}
|
||||
return editorPriority * (this.openByDefault ? 2 : 0.5);
|
||||
}
|
||||
|
||||
protected get openByDefault(): boolean {
|
||||
return this.preferences['preview.openByDefault'];
|
||||
}
|
||||
|
||||
override async open(uri: URI, options?: PreviewOpenerOptions): Promise<PreviewWidget> {
|
||||
const resolvedOptions = await this.resolveOpenerOptions(options);
|
||||
return super.open(uri, resolvedOptions);
|
||||
}
|
||||
protected override serializeUri(uri: URI): string {
|
||||
return super.serializeUri(PreviewUri.decode(uri));
|
||||
}
|
||||
|
||||
protected async resolveOpenerOptions(options?: PreviewOpenerOptions): Promise<PreviewOpenerOptions> {
|
||||
const resolved: PreviewOpenerOptions = { mode: 'activate', ...options };
|
||||
if (resolved.originUri) {
|
||||
const ref = await this.getWidget(resolved.originUri);
|
||||
if (ref) {
|
||||
resolved.widgetOptions = { ...resolved.widgetOptions, ref };
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(PreviewCommands.OPEN, {
|
||||
execute: widget => this.openForEditor(widget),
|
||||
isEnabled: widget => this.canHandleEditorUri(widget),
|
||||
isVisible: widget => this.canHandleEditorUri(widget)
|
||||
});
|
||||
registry.registerCommand(PreviewCommands.OPEN_SOURCE, {
|
||||
execute: widget => this.openSource(widget),
|
||||
isEnabled: widget => widget instanceof PreviewWidget,
|
||||
isVisible: widget => widget instanceof PreviewWidget
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(menus: MenuModelRegistry): void {
|
||||
menus.registerMenuAction(EditorContextMenu.NAVIGATION, {
|
||||
commandId: PreviewCommands.OPEN.id
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(registry: TabBarToolbarRegistry): void {
|
||||
registry.registerItem({
|
||||
id: PreviewCommands.OPEN.id,
|
||||
command: PreviewCommands.OPEN.id,
|
||||
tooltip: nls.localize('vscode.markdown-language-features/package/markdown.previewSide.title', 'Open Preview to the Side')
|
||||
});
|
||||
registry.registerItem({
|
||||
id: PreviewCommands.OPEN_SOURCE.id,
|
||||
command: PreviewCommands.OPEN_SOURCE.id,
|
||||
tooltip: nls.localize('vscode.markdown-language-features/package/markdown.showSource.title', 'Open Source')
|
||||
});
|
||||
}
|
||||
|
||||
protected canHandleEditorUri(widget?: Widget): boolean {
|
||||
const uri = this.getCurrentEditorUri(widget);
|
||||
return !!uri && this.previewHandlerProvider.canHandle(uri);
|
||||
}
|
||||
|
||||
protected getCurrentEditorUri(widget?: Widget): URI | undefined {
|
||||
const current = this.getCurrentEditor(widget);
|
||||
return current && current.editor.uri;
|
||||
}
|
||||
|
||||
protected getCurrentEditor(widget?: Widget): EditorWidget | undefined {
|
||||
const current = widget ? widget : this.editorManager.currentEditor;
|
||||
return current instanceof EditorWidget && current || undefined;
|
||||
}
|
||||
|
||||
protected async openForEditor(widget?: Widget): Promise<void> {
|
||||
const ref = this.getCurrentEditor(widget);
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
await this.open(ref.editor.uri, {
|
||||
mode: 'reveal',
|
||||
widgetOptions: { ref, mode: 'open-to-right' }
|
||||
});
|
||||
}
|
||||
|
||||
protected async openSource(ref: PreviewWidget): Promise<EditorWidget>;
|
||||
protected async openSource(ref?: Widget): Promise<EditorWidget | undefined> {
|
||||
if (ref instanceof PreviewWidget) {
|
||||
return this.editorManager.open(ref.uri, {
|
||||
widgetOptions: { ref, mode: 'tab-after' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
57
packages/preview/src/browser/preview-frontend-module.ts
Normal file
57
packages/preview/src/browser/preview-frontend-module.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { CommandContribution, MenuContribution, bindContributionProvider, ResourceProvider } from '@theia/core/lib/common';
|
||||
import { OpenHandler, WidgetFactory, FrontendApplicationContribution, NavigatableWidgetOptions } from '@theia/core/lib/browser';
|
||||
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { PreviewContribution } from './preview-contribution';
|
||||
import { PreviewWidget, PreviewWidgetOptions } from './preview-widget';
|
||||
import { PreviewHandler, PreviewHandlerProvider } from './preview-handler';
|
||||
import { PreviewUri } from './preview-uri';
|
||||
import { MarkdownPreviewHandler } from './markdown';
|
||||
import { bindPreviewPreferences } from '../common/preview-preferences';
|
||||
import { PreviewLinkNormalizer } from './preview-link-normalizer';
|
||||
|
||||
import '../../src/browser/style/index.css';
|
||||
import '../../src/browser/markdown/style/index.css';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bindPreviewPreferences(bind);
|
||||
bind(PreviewHandlerProvider).toSelf().inSingletonScope();
|
||||
bindContributionProvider(bind, PreviewHandler);
|
||||
bind(MarkdownPreviewHandler).toSelf().inSingletonScope();
|
||||
bind(PreviewHandler).toService(MarkdownPreviewHandler);
|
||||
bind(PreviewLinkNormalizer).toSelf().inSingletonScope();
|
||||
|
||||
bind(PreviewWidget).toSelf();
|
||||
bind<WidgetFactory>(WidgetFactory).toDynamicValue(ctx => ({
|
||||
id: PreviewUri.id,
|
||||
async createWidget(options: NavigatableWidgetOptions): Promise<PreviewWidget> {
|
||||
const { container } = ctx;
|
||||
const resource = await container.get<ResourceProvider>(ResourceProvider)(new URI(options.uri));
|
||||
const child = container.createChild();
|
||||
child.bind<PreviewWidgetOptions>(PreviewWidgetOptions).toConstantValue({ resource });
|
||||
return child.get(PreviewWidget);
|
||||
}
|
||||
})).inSingletonScope();
|
||||
|
||||
bind(PreviewContribution).toSelf().inSingletonScope();
|
||||
[CommandContribution, MenuContribution, OpenHandler, FrontendApplicationContribution, TabBarToolbarContribution].forEach(serviceIdentifier =>
|
||||
bind(serviceIdentifier).toService(PreviewContribution)
|
||||
);
|
||||
});
|
||||
141
packages/preview/src/browser/preview-handler.ts
Normal file
141
packages/preview/src/browser/preview-handler.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ContributionProvider, isObject, MaybePromise, Prioritizeable } from '@theia/core';
|
||||
|
||||
export const PreviewHandler = Symbol('PreviewHandler');
|
||||
|
||||
/**
|
||||
* The parameters given to the preview handler to render the preview content.
|
||||
*/
|
||||
export interface RenderContentParams {
|
||||
/**
|
||||
* Textual content of the resource.
|
||||
*/
|
||||
content: string;
|
||||
/**
|
||||
* URI identifying the source resource.
|
||||
*/
|
||||
originUri: URI;
|
||||
}
|
||||
|
||||
export namespace RenderContentParams {
|
||||
export function is(params: unknown): params is RenderContentParams {
|
||||
return isObject(params) && 'content' in params && 'originUri' in params;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A PreviewHandler manages the integration of one or more previews.
|
||||
*
|
||||
* It indicates whether a preview shall be rendered for a given resource URI and, if yes, renders the content.
|
||||
* Additionally it optionally provides methods with which the scroll state of the preview and corresponding
|
||||
* editor can be managed.
|
||||
*
|
||||
* See {@link MarkdownPreviewHandler} for an example implementation.
|
||||
*/
|
||||
export interface PreviewHandler {
|
||||
/**
|
||||
* One or more classes which specify the preview widget icon.
|
||||
*/
|
||||
readonly iconClass?: string;
|
||||
/**
|
||||
* Indicates whether and with which priority (larger is better) this preview handler is responsible for the resource identified by the given URI.
|
||||
* If multiple handlers return the same priority it's undefined which one will be used.
|
||||
*
|
||||
* @param uri the URI identifying a resource.
|
||||
*
|
||||
* @returns a number larger than 0 if the handler is applicable, 0 or a negative number otherwise.
|
||||
*/
|
||||
canHandle(uri: URI): number;
|
||||
/**
|
||||
* Render the preview content by returning appropriate HTML.
|
||||
*
|
||||
* @param params information for the handler to render its content.
|
||||
*
|
||||
* @returns the HTMLElement which will be attached to the preview widget.
|
||||
*/
|
||||
renderContent(params: RenderContentParams): MaybePromise<HTMLElement | undefined>;
|
||||
/**
|
||||
* Search and return the HTMLElement which corresponds to the given fragment.
|
||||
* This is used to initially reveal elements identified via the URI fragment.
|
||||
*
|
||||
* @param content the preview widget element containing the content previously rendered by {@link PreviewHandler.renderContent}.
|
||||
* @param fragment the URI fragment for which the corresponding element shall be returned
|
||||
*
|
||||
* @returns the HTMLElement which is part of content and corresponds to the given fragment, undefined otherwise.
|
||||
*/
|
||||
findElementForFragment?(content: HTMLElement, fragment: string): HTMLElement | undefined;
|
||||
/**
|
||||
* Search and return the HTMLElement which corresponds to the given line number.
|
||||
* This is used to scroll the preview when the source editor scrolls.
|
||||
*
|
||||
* @param content the preview widget element containing the previously rendered by {@link PreviewHandler.renderContent}.
|
||||
* @param sourceLine the line number for which the corresponding element shall be returned.
|
||||
*
|
||||
* @returns the HTMLElement which is part of content and corresponds to the given line number, undefined otherwise.
|
||||
*/
|
||||
findElementForSourceLine?(content: HTMLElement, sourceLine: number): HTMLElement | undefined;
|
||||
/**
|
||||
* Returns the line number which corresponds to the preview element at the given offset.
|
||||
* This is used to scroll the source editor when the preview scrolls.
|
||||
*
|
||||
* @param content the preview widget element containing the previously rendered by {@link PreviewHandler.renderContent}.
|
||||
* @param offset the total amount by which the preview widget is scrolled.
|
||||
*
|
||||
* @returns the source line number which corresponds to the preview element at the given offset, undefined otherwise.
|
||||
*/
|
||||
getSourceLineForOffset?(content: HTMLElement, offset: number): number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider managing the available PreviewHandlers.
|
||||
*/
|
||||
@injectable()
|
||||
export class PreviewHandlerProvider {
|
||||
|
||||
constructor(
|
||||
@inject(ContributionProvider) @named(PreviewHandler)
|
||||
protected readonly previewHandlerContributions: ContributionProvider<PreviewHandler>
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Find PreviewHandlers for the given resource identifier.
|
||||
*
|
||||
* @param uri the URI identifying a resource.
|
||||
*
|
||||
* @returns the list of all supported `PreviewHandlers` sorted by their priority.
|
||||
*/
|
||||
findContribution(uri: URI): PreviewHandler[] {
|
||||
const prioritized = Prioritizeable.prioritizeAllSync(this.previewHandlerContributions.getContributions(), contrib =>
|
||||
contrib.canHandle(uri)
|
||||
);
|
||||
return prioritized.map(c => c.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether any PreviewHandler can process the resource identified by the given URI.
|
||||
*
|
||||
* @param uri the URI identifying a resource.
|
||||
*
|
||||
* @returns `true` when a PreviewHandler can process the resource, `false` otherwise.
|
||||
*/
|
||||
canHandle(uri: URI): boolean {
|
||||
return this.findContribution(uri).length > 0;
|
||||
}
|
||||
}
|
||||
40
packages/preview/src/browser/preview-link-normalizer.ts
Normal file
40
packages/preview/src/browser/preview-link-normalizer.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { MiniBrowserEnvironment } from '@theia/mini-browser/lib/browser/environment/mini-browser-environment';
|
||||
|
||||
@injectable()
|
||||
export class PreviewLinkNormalizer {
|
||||
|
||||
protected urlScheme = new RegExp('^[a-z][a-z|0-9|\+|\-|\.]*:', 'i');
|
||||
|
||||
@inject(MiniBrowserEnvironment)
|
||||
protected readonly miniBrowserEnvironment: MiniBrowserEnvironment;
|
||||
|
||||
normalizeLink(documentUri: URI, link: string): string {
|
||||
try {
|
||||
if (!this.urlScheme.test(link)) {
|
||||
const location = documentUri.parent.resolve(link).path.toString();
|
||||
return this.miniBrowserEnvironment.getEndpoint('normalized-link').getRestUrl().resolve(location).toString();
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return link;
|
||||
}
|
||||
}
|
||||
43
packages/preview/src/browser/preview-uri.ts
Normal file
43
packages/preview/src/browser/preview-uri.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
|
||||
export namespace PreviewUri {
|
||||
export const id = 'code-editor-preview';
|
||||
export const param = 'open-handler=' + id;
|
||||
export function match(uri: URI): boolean {
|
||||
return uri.query.indexOf(param) !== -1;
|
||||
}
|
||||
export function encode(uri: URI): URI {
|
||||
if (match(uri)) {
|
||||
return uri;
|
||||
}
|
||||
const params = [param];
|
||||
if (uri.query) {
|
||||
params.push(...uri.query.split('&'));
|
||||
}
|
||||
const query = params.join('&');
|
||||
return uri.withQuery(query);
|
||||
}
|
||||
export function decode(uri: URI): URI {
|
||||
if (!match(uri)) {
|
||||
return uri;
|
||||
}
|
||||
const query = uri.query.split('&').filter(p => p !== param).join('&');
|
||||
return uri.withQuery(query);
|
||||
}
|
||||
}
|
||||
277
packages/preview/src/browser/preview-widget.ts
Normal file
277
packages/preview/src/browser/preview-widget.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import throttle = require('@theia/core/shared/lodash.throttle');
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { Resource, MaybePromise } from '@theia/core';
|
||||
import { Navigatable } from '@theia/core/lib/browser/navigatable';
|
||||
import { BaseWidget, Message, addEventListener, codicon } from '@theia/core/lib/browser';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Event, Emitter } from '@theia/core/lib/common';
|
||||
import { PreviewHandler, PreviewHandlerProvider } from './preview-handler';
|
||||
import { ThemeService } from '@theia/core/lib/browser/theming';
|
||||
import { EditorPreferences } from '@theia/editor/lib/common/editor-preferences';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
|
||||
import { Range, Location } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
|
||||
export const PREVIEW_WIDGET_CLASS = 'theia-preview-widget';
|
||||
|
||||
const DEFAULT_ICON = codicon('eye');
|
||||
|
||||
let widgetCounter: number = 0;
|
||||
|
||||
export const PreviewWidgetOptions = Symbol('PreviewWidgetOptions');
|
||||
export interface PreviewWidgetOptions {
|
||||
resource: Resource
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PreviewWidget extends BaseWidget implements Navigatable {
|
||||
|
||||
readonly uri: URI;
|
||||
protected readonly resource: Resource;
|
||||
protected previewHandler: PreviewHandler | undefined;
|
||||
protected firstUpdate: (() => void) | undefined = undefined;
|
||||
protected readonly onDidScrollEmitter = new Emitter<number>();
|
||||
protected readonly onDidDoubleClickEmitter = new Emitter<Location>();
|
||||
protected scrollBeyondLastLine: boolean;
|
||||
|
||||
constructor(
|
||||
@inject(PreviewWidgetOptions) protected readonly options: PreviewWidgetOptions,
|
||||
@inject(PreviewHandlerProvider) protected readonly previewHandlerProvider: PreviewHandlerProvider,
|
||||
@inject(ThemeService) protected readonly themeService: ThemeService,
|
||||
@inject(MonacoWorkspace) protected readonly workspace: MonacoWorkspace,
|
||||
@inject(EditorPreferences) protected readonly editorPreferences: EditorPreferences,
|
||||
) {
|
||||
super();
|
||||
this.resource = this.options.resource;
|
||||
this.uri = this.resource.uri;
|
||||
this.id = 'preview-widget-' + widgetCounter++;
|
||||
this.title.closable = true;
|
||||
this.title.label = `Preview ${this.uri.path.base}`;
|
||||
this.title.caption = this.title.label;
|
||||
this.title.closable = true;
|
||||
|
||||
this.toDispose.push(this.onDidScrollEmitter);
|
||||
this.toDispose.push(this.onDidDoubleClickEmitter);
|
||||
|
||||
this.addClass(PREVIEW_WIDGET_CLASS);
|
||||
this.node.tabIndex = 0;
|
||||
const previewHandler = this.previewHandler = this.previewHandlerProvider.findContribution(this.uri)[0];
|
||||
if (!previewHandler) {
|
||||
return;
|
||||
}
|
||||
this.title.iconClass = previewHandler.iconClass || DEFAULT_ICON;
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
this.scrollBeyondLastLine = !!this.editorPreferences['editor.scrollBeyondLastLine'];
|
||||
this.toDispose.push(this.editorPreferences.onPreferenceChanged(e => {
|
||||
if (e.preferenceName === 'editor.scrollBeyondLastLine') {
|
||||
this.scrollBeyondLastLine = !!this.editorPreferences['editor.scrollBeyondLastLine'];
|
||||
this.forceUpdate();
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(this.resource);
|
||||
if (this.resource.onDidChangeContents) {
|
||||
this.toDispose.push(this.resource.onDidChangeContents(() => this.update()));
|
||||
}
|
||||
const updateIfAffected = (affectedUri?: string) => {
|
||||
if (!affectedUri || affectedUri === this.uri.toString()) {
|
||||
this.update();
|
||||
}
|
||||
};
|
||||
this.toDispose.push(this.workspace.onDidOpenTextDocument(document => updateIfAffected(document.uri)));
|
||||
this.toDispose.push(this.workspace.onDidChangeTextDocument(params => updateIfAffected(params.model.uri)));
|
||||
this.toDispose.push(this.workspace.onDidCloseTextDocument(document => updateIfAffected(document.uri)));
|
||||
this.toDispose.push(this.themeService.onDidColorThemeChange(() => this.update()));
|
||||
this.firstUpdate = () => {
|
||||
this.revealFragment(this.uri);
|
||||
};
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected override onBeforeAttach(msg: Message): void {
|
||||
super.onBeforeAttach(msg);
|
||||
this.toDispose.push(this.startScrollSync());
|
||||
this.toDispose.push(this.startDoubleClickListener());
|
||||
}
|
||||
|
||||
protected preventScrollNotification: boolean = false;
|
||||
protected startScrollSync(): Disposable {
|
||||
return addEventListener(this.node, 'scroll', throttle((event: UIEvent) => {
|
||||
if (this.preventScrollNotification) {
|
||||
return;
|
||||
}
|
||||
const scrollTop = this.node.scrollTop;
|
||||
this.didScroll(scrollTop);
|
||||
}, 50));
|
||||
}
|
||||
|
||||
protected startDoubleClickListener(): Disposable {
|
||||
return addEventListener(this.node, 'dblclick', (event: MouseEvent) => {
|
||||
if (!(event.target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const target = event.target as HTMLElement;
|
||||
let node: HTMLElement | null = target;
|
||||
while (node && node instanceof HTMLElement) {
|
||||
if (node.tagName === 'A') {
|
||||
return;
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
const offsetParent = target.offsetParent as HTMLElement;
|
||||
const offset = offsetParent.classList.contains(PREVIEW_WIDGET_CLASS) ? target.offsetTop : offsetParent.offsetTop;
|
||||
this.didDoubleClick(offset);
|
||||
});
|
||||
}
|
||||
|
||||
getUri(): URI {
|
||||
return this.uri;
|
||||
}
|
||||
|
||||
getResourceUri(): URI | undefined {
|
||||
return this.uri;
|
||||
}
|
||||
createMoveToUri(resourceUri: URI): URI | undefined {
|
||||
return this.uri.withPath(resourceUri.path);
|
||||
}
|
||||
|
||||
override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
this.node.focus();
|
||||
this.update();
|
||||
}
|
||||
|
||||
override onUpdateRequest(msg: Message): void {
|
||||
super.onUpdateRequest(msg);
|
||||
this.performUpdate();
|
||||
}
|
||||
|
||||
protected forceUpdate(): void {
|
||||
this.previousContent = undefined;
|
||||
this.update();
|
||||
}
|
||||
|
||||
protected previousContent: string | undefined = undefined;
|
||||
protected async performUpdate(): Promise<void> {
|
||||
if (!this.resource) {
|
||||
return;
|
||||
}
|
||||
const uri = this.resource.uri;
|
||||
const document = this.workspace.textDocuments.find(d => d.uri === uri.toString());
|
||||
const content: MaybePromise<string> = document ? document.getText() : await this.resource.readContents();
|
||||
if (content === this.previousContent) {
|
||||
return;
|
||||
}
|
||||
this.previousContent = content;
|
||||
const contentElement = await this.render(content, uri);
|
||||
this.node.innerHTML = '';
|
||||
if (contentElement) {
|
||||
if (this.scrollBeyondLastLine) {
|
||||
contentElement.classList.add('scrollBeyondLastLine');
|
||||
}
|
||||
this.node.appendChild(contentElement);
|
||||
if (this.firstUpdate) {
|
||||
this.firstUpdate();
|
||||
this.firstUpdate = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async render(content: string, originUri: URI): Promise<HTMLElement | undefined> {
|
||||
if (!this.previewHandler || !this.resource) {
|
||||
return undefined;
|
||||
}
|
||||
return this.previewHandler.renderContent({ content, originUri });
|
||||
}
|
||||
|
||||
protected revealFragment(uri: URI): void {
|
||||
if (uri.fragment === '' || !this.previewHandler || !this.previewHandler.findElementForFragment) {
|
||||
return;
|
||||
}
|
||||
const elementToReveal = this.previewHandler.findElementForFragment(this.node, uri.fragment);
|
||||
if (elementToReveal) {
|
||||
this.preventScrollNotification = true;
|
||||
elementToReveal.scrollIntoView();
|
||||
window.setTimeout(() => {
|
||||
this.preventScrollNotification = false;
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
revealForSourceLine(sourceLine: number): void {
|
||||
this.internalRevealForSourceLine(sourceLine);
|
||||
}
|
||||
protected readonly internalRevealForSourceLine: (sourceLine: number) => void = throttle((sourceLine: number) => {
|
||||
if (!this.previewHandler || !this.previewHandler.findElementForSourceLine) {
|
||||
return;
|
||||
}
|
||||
const elementToReveal = this.previewHandler.findElementForSourceLine(this.node, sourceLine);
|
||||
if (elementToReveal) {
|
||||
this.preventScrollNotification = true;
|
||||
elementToReveal.scrollIntoView();
|
||||
window.setTimeout(() => {
|
||||
this.preventScrollNotification = false;
|
||||
}, 50);
|
||||
}
|
||||
}, 50);
|
||||
|
||||
get onDidScroll(): Event<number> {
|
||||
return this.onDidScrollEmitter.event;
|
||||
}
|
||||
|
||||
protected fireDidScrollToSourceLine(line: number): void {
|
||||
this.onDidScrollEmitter.fire(line);
|
||||
}
|
||||
|
||||
protected didScroll(scrollTop: number): void {
|
||||
if (!this.previewHandler || !this.previewHandler.getSourceLineForOffset) {
|
||||
return;
|
||||
}
|
||||
const offset = scrollTop;
|
||||
const line = this.previewHandler.getSourceLineForOffset(this.node, offset);
|
||||
if (line) {
|
||||
this.fireDidScrollToSourceLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
get onDidDoubleClick(): Event<Location> {
|
||||
return this.onDidDoubleClickEmitter.event;
|
||||
}
|
||||
|
||||
protected fireDidDoubleClickToSourceLine(line: number): void {
|
||||
if (!this.resource) {
|
||||
return;
|
||||
}
|
||||
this.onDidDoubleClickEmitter.fire({
|
||||
uri: this.resource.uri.toString(),
|
||||
range: Range.create({ line, character: 0 }, { line, character: 0 })
|
||||
});
|
||||
}
|
||||
|
||||
protected didDoubleClick(offsetTop: number): void {
|
||||
if (!this.previewHandler || !this.previewHandler.getSourceLineForOffset) {
|
||||
return;
|
||||
}
|
||||
const line = this.previewHandler.getSourceLineForOffset(this.node, offsetTop) || 0;
|
||||
this.fireDidDoubleClickToSourceLine(line);
|
||||
}
|
||||
|
||||
}
|
||||
17
packages/preview/src/browser/style/index.css
Normal file
17
packages/preview/src/browser/style/index.css
Normal file
@@ -0,0 +1,17 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2018 TypeFox and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
@import "./preview-widget.css";
|
||||
29
packages/preview/src/browser/style/preview-widget.css
Normal file
29
packages/preview/src/browser/style/preview-widget.css
Normal file
@@ -0,0 +1,29 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2018 TypeFox and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
.theia-preview-widget {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.theia-preview-widget .scrollBeyondLastLine {
|
||||
margin-bottom: calc(100vh - var(--theia-content-line-height));
|
||||
}
|
||||
|
||||
.theia-preview-widget:focus {
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
53
packages/preview/src/common/preview-preferences.ts
Normal file
53
packages/preview/src/common/preview-preferences.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { PreferenceService } from '@theia/core/lib/common/preferences/preference-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { createPreferenceProxy, PreferenceProxy } from '@theia/core/lib/common/preferences/preference-proxy';
|
||||
import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
|
||||
|
||||
export const PreviewConfigSchema: PreferenceSchema = {
|
||||
properties: {
|
||||
'preview.openByDefault': {
|
||||
type: 'boolean',
|
||||
description: nls.localize('theia/preview/openByDefault', 'Open the preview instead of the editor by default.'),
|
||||
default: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export interface PreviewConfiguration {
|
||||
'preview.openByDefault': boolean;
|
||||
}
|
||||
|
||||
export const PreviewPreferenceContribution = Symbol('PreviewPreferenceContribution');
|
||||
export const PreviewPreferences = Symbol('PreviewPreferences');
|
||||
export type PreviewPreferences = PreferenceProxy<PreviewConfiguration>;
|
||||
|
||||
export function createPreviewPreferences(preferences: PreferenceService, schema: PreferenceSchema = PreviewConfigSchema): PreviewPreferences {
|
||||
return createPreferenceProxy(preferences, schema);
|
||||
}
|
||||
|
||||
export function bindPreviewPreferences(bind: interfaces.Bind): void {
|
||||
bind(PreviewPreferences).toDynamicValue(ctx => {
|
||||
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
|
||||
const contribution = ctx.container.get<PreferenceContribution>(PreviewPreferenceContribution);
|
||||
return createPreviewPreferences(preferences, contribution.schema);
|
||||
}).inSingletonScope();
|
||||
bind(PreviewPreferenceContribution).toConstantValue({ schema: PreviewConfigSchema });
|
||||
bind(PreferenceContribution).toService(PreviewPreferenceContribution);
|
||||
}
|
||||
22
packages/preview/src/node/preview-backend-module.ts
Normal file
22
packages/preview/src/node/preview-backend-module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 STMicroelectronics and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { bindPreviewPreferences } from '../common/preview-preferences';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bindPreviewPreferences(bind);
|
||||
});
|
||||
29
packages/preview/src/package.spec.ts
Normal file
29
packages/preview/src/package.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* note: this bogus test file is required so that
|
||||
we are able to run mocha unit tests on this
|
||||
package, without having any actual unit tests in it.
|
||||
This way a coverage report will be generated,
|
||||
showing 0% coverage, instead of no report.
|
||||
This file can be removed once we have real unit
|
||||
tests in place. */
|
||||
|
||||
describe('preview package', () => {
|
||||
|
||||
it('support code coverage statistics', () => true);
|
||||
|
||||
});
|
||||
25
packages/preview/tsconfig.json
Normal file
25
packages/preview/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../editor"
|
||||
},
|
||||
{
|
||||
"path": "../mini-browser"
|
||||
},
|
||||
{
|
||||
"path": "../monaco"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user