提交 8d7504f7 编写于 作者: M Martin Aeschlimann

[html] add link provider

上级 99ebc306
......@@ -9,7 +9,8 @@
},
"dependencies": {
"vscode-languageserver": "^2.4.0-next.12",
"vscode-nls": "^1.0.4"
"vscode-nls": "^1.0.4",
"vscode-uri": "^0.0.7"
},
"scripts": {
"compile": "gulp compile-extension:json-server",
......
......@@ -6,12 +6,28 @@
import {parse} from './parser/htmlParser';
import {doComplete} from './services/htmlCompletion';
import {format} from './services/htmlFormatter';
import {provideLinks} from './services/htmlLinks';
import {findDocumentHighlights} from './services/htmlHighlighting';
import {TextDocument, Position, CompletionItem, CompletionList, Hover, Range, SymbolInformation, Diagnostic, TextEdit, DocumentHighlight, FormattingOptions, MarkedString } from 'vscode-languageserver-types';
export {TextDocument, Position, CompletionItem, CompletionList, Hover, Range, SymbolInformation, Diagnostic, TextEdit, DocumentHighlight, FormattingOptions, MarkedString };
export class DocumentLink {
/**
* The range this link applies to.
*/
range: Range;
/**
* The uri this link points to.
*/
target: string;
}
export interface HTMLFormatConfiguration {
tabSize: number;
insertSpaces: boolean;
......@@ -38,8 +54,8 @@ export interface LanguageService {
doValidation(document: TextDocument, htmlDocument: HTMLDocument): Diagnostic[];
findDocumentHighlights(document: TextDocument, position: Position, htmlDocument: HTMLDocument): DocumentHighlight[];
doComplete(document: TextDocument, position: Position, htmlDocument: HTMLDocument): CompletionList;
// doHover(document: TextDocument, position: Position, doc: HTMLDocument): Hover;
format(document: TextDocument, range: Range, options: HTMLFormatConfiguration): TextEdit[];
provideLinks(document: TextDocument, workspacePath:string): DocumentLink[];
}
export function getLanguageService() : LanguageService {
......@@ -49,6 +65,7 @@ export function getLanguageService() : LanguageService {
parseHTMLDocument: (document) => parse(document.getText()),
doComplete,
format,
findDocumentHighlights
findDocumentHighlights,
provideLinks
};
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {TokenType, createScanner} from '../parser/htmlScanner';
import {TextDocument, Range} from 'vscode-languageserver-types';
import * as paths from '../utils/paths';
import * as strings from '../utils/strings';
import Uri from 'vscode-uri';
import {DocumentLink} from '../htmlLanguageService';
function _stripQuotes(url: string): string {
return url
.replace(/^'([^']+)'$/,(substr, match1) => match1)
.replace(/^"([^"]+)"$/,(substr, match1) => match1);
}
export function _getWorkspaceUrl(modelAbsoluteUri: Uri, rootAbsoluteUrlStr: string, tokenContent: string): string {
tokenContent = _stripQuotes(tokenContent);
if (/^\s*javascript\:/i.test(tokenContent) || /^\s*\#/i.test(tokenContent)) {
return null;
}
if (/^\s*https?:\/\//i.test(tokenContent) || /^\s*file:\/\//i.test(tokenContent)) {
// Absolute link that needs no treatment
return tokenContent.replace(/^\s*/g, '');
}
if (/^\s*\/\//i.test(tokenContent)) {
// Absolute link (that does not name the protocol)
let pickedScheme = 'http';
if (modelAbsoluteUri.scheme === 'https') {
pickedScheme = 'https';
}
return pickedScheme + ':' + tokenContent.replace(/^\s*/g, '');
}
let modelPath = paths.dirname(modelAbsoluteUri.path);
let alternativeResultPath: string = null;
if (tokenContent.length > 0 && tokenContent.charAt(0) === '/') {
alternativeResultPath = tokenContent;
} else {
alternativeResultPath = paths.join(modelPath, tokenContent);
alternativeResultPath = alternativeResultPath.replace(/^(\/\.\.)+/, '');
}
let potentialResult = modelAbsoluteUri.with({ path: alternativeResultPath }).toString();
if (rootAbsoluteUrlStr && strings.startsWith(modelAbsoluteUri.toString(), rootAbsoluteUrlStr)) {
// The `rootAbsoluteUrl` is set and matches our current model
// We need to ensure that this `potentialResult` does not escape `rootAbsoluteUrl`
let commonPrefixLength = strings.commonPrefixLength(rootAbsoluteUrlStr, potentialResult);
if (strings.endsWith(rootAbsoluteUrlStr, '/')) {
commonPrefixLength = potentialResult.lastIndexOf('/', commonPrefixLength) + 1;
}
return rootAbsoluteUrlStr + potentialResult.substr(commonPrefixLength);
}
return potentialResult;
}
function createLink(document: TextDocument, rootAbsoluteUrl: string, tokenContent: string, startOffset: number, endOffset: number): DocumentLink {
let documentUri = Uri.parse(document.uri);
let workspaceUrl = _getWorkspaceUrl(documentUri, rootAbsoluteUrl, tokenContent);
if (!workspaceUrl) {
return null;
}
return {
range: Range.create(document.positionAt(startOffset), document.positionAt(endOffset)),
target: workspaceUrl
};
}
export function provideLinks(document: TextDocument, workspacePath:string): DocumentLink[] {
let newLinks: DocumentLink[] = [];
let rootAbsoluteUrl: string = null;
if (workspacePath) {
// The workspace can be null in the no folder opened case
let strRootAbsoluteUrl = workspacePath;
if (strRootAbsoluteUrl.charAt(strRootAbsoluteUrl.length - 1) === '/') {
rootAbsoluteUrl = strRootAbsoluteUrl;
} else {
rootAbsoluteUrl = strRootAbsoluteUrl + '/';
}
}
let scanner = createScanner(document.getText(), 0);
let token = scanner.scan();
let afterHrefOrSrc = false;
while (token !== TokenType.EOS) {
switch (token) {
case TokenType.AttributeName:
let tokenContent = scanner.getTokenText();
afterHrefOrSrc = tokenContent === 'src' || tokenContent === 'href';
break;
case TokenType.AttributeValue:
if (afterHrefOrSrc) {
let tokenContent = scanner.getTokenText();
let link = createLink(document, rootAbsoluteUrl, tokenContent, scanner.getTokenOffset(), scanner.getTokenEnd());
if (link) {
newLinks.push(link);
}
afterHrefOrSrc = false;
}
break;
}
}
return newLinks;
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import * as htmlLinks from '../services/htmlLinks';
import {CompletionList, TextDocument, TextEdit, Position, CompletionItemKind} from 'vscode-languageserver-types';
import Uri from 'vscode-uri';
suite('HTML Link Detection', () => {
function testLinkCreation(modelUrl:string, rootUrl:string, tokenContent:string, expected:string): void {
var _modelUrl = Uri.parse(modelUrl);
var actual = htmlLinks._getWorkspaceUrl(_modelUrl, rootUrl, tokenContent);
assert.equal(actual, expected);
}
test('Link creation', () => {
testLinkCreation('inmemory://model/1', null, 'javascript:void;', null);
testLinkCreation('inmemory://model/1', null, ' \tjavascript:alert(7);', null);
testLinkCreation('inmemory://model/1', null, ' #relative', null);
testLinkCreation('inmemory://model/1', null, 'file:///C:\\Alex\\src\\path\\to\\file.txt', 'file:///C:\\Alex\\src\\path\\to\\file.txt');
testLinkCreation('inmemory://model/1', null, 'http://www.microsoft.com/', 'http://www.microsoft.com/');
testLinkCreation('inmemory://model/1', null, 'https://www.microsoft.com/', 'https://www.microsoft.com/');
testLinkCreation('inmemory://model/1', null, '//www.microsoft.com/', 'http://www.microsoft.com/');
testLinkCreation('inmemory://model/1', null, '../../a.js', 'inmemory://model/a.js');
testLinkCreation('inmemory://model/1', 'inmemory://model/', 'javascript:void;', null);
testLinkCreation('inmemory://model/1', 'inmemory://model/', ' \tjavascript:alert(7);', null);
testLinkCreation('inmemory://model/1', 'inmemory://model/', ' #relative', null);
testLinkCreation('inmemory://model/1', 'inmemory://model/', 'file:///C:\\Alex\\src\\path\\to\\file.txt', 'file:///C:\\Alex\\src\\path\\to\\file.txt');
testLinkCreation('inmemory://model/1', 'inmemory://model/', 'http://www.microsoft.com/', 'http://www.microsoft.com/');
testLinkCreation('inmemory://model/1', 'inmemory://model/', 'https://www.microsoft.com/', 'https://www.microsoft.com/');
testLinkCreation('inmemory://model/1', 'inmemory://model/', ' //www.microsoft.com/', 'http://www.microsoft.com/');
testLinkCreation('inmemory://model/1', 'inmemory://model/', '../../a.js', 'inmemory://model/a.js');
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', null, 'javascript:void;', null);
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', null, ' \tjavascript:alert(7);', null);
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', null, ' #relative', null);
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', null, 'file:///C:\\Alex\\src\\path\\to\\file.txt', 'file:///C:\\Alex\\src\\path\\to\\file.txt');
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', null, 'http://www.microsoft.com/', 'http://www.microsoft.com/');
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', null, 'https://www.microsoft.com/', 'https://www.microsoft.com/');
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', null, ' //www.microsoft.com/', 'http://www.microsoft.com/');
//testLinkCreation('file:///C:/Alex/src/path/to/file.txt', null, 'a.js', 'file:///C:/Alex/src/path/to/a.js');
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', null, '/a.js', 'file:///a.js');
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', 'file:///C:/Alex/src/', 'javascript:void;', null);
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', 'file:///C:/Alex/src/', ' \tjavascript:alert(7);', null);
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', 'file:///C:/Alex/src/', ' #relative', null);
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', null, 'file:///C:\\Alex\\src\\path\\to\\file.txt', 'file:///C:\\Alex\\src\\path\\to\\file.txt');
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', 'file:///C:/Alex/src/', 'http://www.microsoft.com/', 'http://www.microsoft.com/');
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', 'file:///C:/Alex/src/', 'https://www.microsoft.com/', 'https://www.microsoft.com/');
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', 'file:///C:/Alex/src/', 'https://www.microsoft.com/?q=1#h', 'https://www.microsoft.com/?q=1#h');
testLinkCreation('file:///C:/Alex/src/path/to/file.txt', 'file:///C:/Alex/src/', ' //www.microsoft.com/', 'http://www.microsoft.com/');
//testLinkCreation('file:///C:/Alex/src/path/to/file.txt', 'file:///C:/Alex/src/', 'a.js', 'file:///C:/Alex/src/path/to/a.js');
//testLinkCreation('file:///C:/Alex/src/path/to/file.txt', 'file:///C:/Alex/src/', '/a.js', 'file:///C:/Alex/src/a.js');
testLinkCreation('https://www.test.com/path/to/file.txt', null, 'file:///C:\\Alex\\src\\path\\to\\file.txt', 'file:///C:\\Alex\\src\\path\\to\\file.txt');
testLinkCreation('https://www.test.com/path/to/file.txt', null, '//www.microsoft.com/', 'https://www.microsoft.com/');
testLinkCreation('https://www.test.com/path/to/file.txt', 'https://www.test.com', '//www.microsoft.com/', 'https://www.microsoft.com/');
// invalid uris don't throw
testLinkCreation('https://www.test.com/path/to/file.txt', 'https://www.test.com', '%', 'https://www.test.com/path/to/%25');
// Bug #18314: Ctrl + Click does not open existing file if folder's name starts with 'c' character
// testLinkCreation('file:///c:/Alex/working_dir/18314-link-detection/test.html', 'file:///c:/Alex/working_dir/18314-link-detection/', '/class/class.js', 'file:///c:/Alex/working_dir/18314-link-detection/class/class.js');
});
});
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export const enum CharCode {
Slash = 47,
Backslash = 92
}
/**
* @returns the directory name of a path.
*/
export function dirname(path: string): string {
var idx = ~path.lastIndexOf('/') || ~path.lastIndexOf('\\');
if (idx === 0) {
return '.';
} else if (~idx === 0) {
return path[0];
} else {
return path.substring(0, ~idx);
}
}
/**
* @returns the base name of a path.
*/
export function basename(path: string): string {
var idx = ~path.lastIndexOf('/') || ~path.lastIndexOf('\\');
if (idx === 0) {
return path;
} else if (~idx === path.length - 1) {
return basename(path.substring(0, path.length - 1));
} else {
return path.substr(~idx + 1);
}
}
/**
* @returns {{.far}} from boo.far or the empty string.
*/
export function extname(path: string): string {
path = basename(path);
var idx = ~path.lastIndexOf('.');
return idx ? path.substring(~idx) : '';
}
export const join: (...parts: string[]) => string = function () {
// Not using a function with var-args because of how TS compiles
// them to JS - it would result in 2*n runtime cost instead
// of 1*n, where n is parts.length.
let value = '';
for (let i = 0; i < arguments.length; i++) {
let part = arguments[i];
if (i > 0) {
// add the separater between two parts unless
// there already is one
let last = value.charCodeAt(value.length - 1);
if (last !== CharCode.Slash && last !== CharCode.Backslash) {
let next = part.charCodeAt(0);
if (next !== CharCode.Slash && next !== CharCode.Backslash) {
value += '/';
}
}
}
value += part;
}
return value;
};
......@@ -32,6 +32,19 @@ export function endsWith(haystack: string, needle: string): boolean {
}
}
export function convertSimple2RegExpPattern(pattern: string): string {
return pattern.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&').replace(/[\*]/g, '.*');
}
/**
* @returns the length of the common prefix of the two strings.
*/
export function commonPrefixLength(a: string, b: string): number {
let i: number,
len = Math.min(a.length, b.length);
for (i = 0; i < len; i++) {
if (a.charCodeAt(i) !== b.charCodeAt(i)) {
return i;
}
}
return len;
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册