deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

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

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

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

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

View File

@@ -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
![alternativetext](subfolder/image.png)
`;
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;
}

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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"
}
]
}