mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 21:09:43 +00:00
[html] add link provider
This commit is contained in:
parent
99ebc3065a
commit
8d7504f7e1
|
@ -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
|
||||
};
|
||||
}
|
114
extensions/html/server/src/service/services/htmlLinks.ts
Normal file
114
extensions/html/server/src/service/services/htmlLinks.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
71
extensions/html/server/src/service/test/links.test.ts
Normal file
71
extensions/html/server/src/service/test/links.test.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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');
|
||||
});
|
||||
});
|
76
extensions/html/server/src/service/utils/paths.ts
Normal file
76
extensions/html/server/src/service/utils/paths.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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;
|
||||
}
|
Loading…
Reference in a new issue