New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding line and column support for terminal #24832
Changes from all commits
425ed1b
441099c
8d5be51
566e2d7
325d9a7
94affbf
e848c21
3247d10
4f6cb99
3984e25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ import Uri from 'vs/base/common/uri'; | |
import { dispose, IDisposable } from 'vs/base/common/lifecycle'; | ||
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; | ||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; | ||
import { IOpenerService } from 'vs/platform/opener/common/opener'; | ||
import { TerminalWidgetManager } from 'vs/workbench/parts/terminal/browser/terminalWidgetManager'; | ||
import { TPromise } from 'vs/base/common/winjs.base'; | ||
|
||
|
@@ -20,13 +21,31 @@ const pathSeparatorClause = '\\/'; | |
const excludedPathCharactersClause = '[^\\0\\s!$`&*()\\[\\]+\'":;]'; // '":; are allowed in paths but they are often separators so ignore them | ||
const escapedExcludedPathCharactersClause = '(\\\\s|\\\\!|\\\\$|\\\\`|\\\\&|\\\\*|(|)|\\+)'; | ||
/** A regex that matches paths in the form /foo, ~/foo, ./foo, ../foo, foo/bar */ | ||
const UNIX_LIKE_LOCAL_LINK_REGEX = new RegExp('((' + pathPrefix + '|(' + excludedPathCharactersClause + '|' + escapedExcludedPathCharactersClause + ')+)?(' + pathSeparatorClause + '(' + excludedPathCharactersClause + '|' + escapedExcludedPathCharactersClause + ')+)+)'); | ||
const unixLocalLinkClause = '((' + pathPrefix + '|(' + excludedPathCharactersClause + '|' + escapedExcludedPathCharactersClause + ')+)?(' + pathSeparatorClause + '(' + excludedPathCharactersClause + '|' + escapedExcludedPathCharactersClause + ')+)+)'; | ||
|
||
const winDrivePrefix = '[a-zA-Z]:'; | ||
const winPathPrefix = '(' + winDrivePrefix + '|\\.\\.?|\\~)'; | ||
const winPathSeparatorClause = '(\\\\|\\/)'; | ||
const winExcludedPathCharactersClause = '[^\\0<>\\?\\|\\/\\s!$`&*()\\[\\]+\'":;]'; | ||
/** A regex that matches paths in the form c:\foo, ~\foo, .\foo, ..\foo, foo\bar */ | ||
const WINDOWS_LOCAL_LINK_REGEX = new RegExp('((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)'); | ||
const winLocalLinkClause = '((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)'; | ||
|
||
/** As xterm reads from DOM, space in that case is nonbreaking char ASCII code - 160, | ||
replacing space with nonBreakningSpace or space ASCII code - 32. */ | ||
const lineAndColumnClause = [ | ||
'((\\S*) on line ((\\d+)(, column (\\d+))?))', // (file path) on line 8, column 13 | ||
'((\\S*):line ((\\d+)(, column (\\d+))?))', // (file path):line 8, column 13 | ||
'(([^\\s\\(\\)]*)(\\s?\\((\\d+)(,(\\d+))?)\\))', // (file path)(45), (file path) (45), (file path)(45,18), (file path) (45,18) | ||
'(([^:\\s\\(\\)<>\'\"\\[\\]]*)(:(\\d+))?(:(\\d+))?)' // (file path):336, (file path):336:9 | ||
].join('|').replace(/ /g, `[${'\u00A0'} ]`); | ||
|
||
// Changing any regex may effect this value, hence changes this as well if required. | ||
const winLineAndColumnMatchIndex = 12; | ||
const unixLineAndColumnMatchIndex = 15; | ||
|
||
// Each line and column clause have 6 groups (ie no. of expressions in round brackets) | ||
const lineAndColumnClauseGroupCount = 6; | ||
|
||
/** Higher than local link, lower than hypertext */ | ||
const CUSTOM_LINK_PRIORITY = -1; | ||
/** Lowest */ | ||
|
@@ -39,12 +58,19 @@ export class TerminalLinkHandler { | |
private _tooltipDisposables: IDisposable[] = []; | ||
private _widgetManager: TerminalWidgetManager; | ||
|
||
private _localLinkPattern: RegExp; | ||
|
||
constructor( | ||
private _xterm: any, | ||
private _platform: platform.Platform, | ||
@IOpenerService private _openerService: IOpenerService, | ||
@IWorkbenchEditorService private _editorService: IWorkbenchEditorService, | ||
@IWorkspaceContextService private _contextService: IWorkspaceContextService | ||
) { | ||
const baseLocalLinkClause = _platform === platform.Platform.Windows ? winLocalLinkClause : unixLocalLinkClause; | ||
// Append line and column number regex | ||
this._localLinkPattern = new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`); | ||
|
||
this._xterm.setHypertextLinkHandler(this._wrapLinkHandler(() => true)); | ||
this._xterm.setHypertextValidationCallback((uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => { | ||
this._validateWebLink(uri, element, callback); | ||
|
@@ -76,8 +102,8 @@ export class TerminalLinkHandler { | |
this._handleLocalLink(url); | ||
return; | ||
}); | ||
|
||
return this._xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, { | ||
matchIndex: 1, | ||
validationCallback: (link: string, element: HTMLElement, callback: (isValid: boolean) => void) => this._validateLocalLink(link, element, callback), | ||
priority: LOCAL_LINK_PRIORITY | ||
}); | ||
|
@@ -99,19 +125,26 @@ export class TerminalLinkHandler { | |
} | ||
|
||
protected get _localLinkRegex(): RegExp { | ||
if (this._platform === platform.Platform.Windows) { | ||
return WINDOWS_LOCAL_LINK_REGEX; | ||
} | ||
return UNIX_LIKE_LOCAL_LINK_REGEX; | ||
return this._localLinkPattern; | ||
} | ||
|
||
private _handleLocalLink(link: string): TPromise<void> { | ||
return this._resolvePath(link).then(resolvedLink => { | ||
if (!resolvedLink) { | ||
return void 0; | ||
} | ||
const resource = Uri.file(path.normalize(path.resolve(resolvedLink))); | ||
return this._editorService.openEditor({ resource }).then(() => void 0); | ||
|
||
let normalizedPath = path.normalize(path.resolve(resolvedLink)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be pulled into a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that will be better will do that |
||
const normalizedUrl = this.extractLinkUrl(normalizedPath); | ||
|
||
normalizedPath = this._formatLocalLinkPath(normalizedPath); | ||
|
||
let resource = Uri.file(normalizedUrl); | ||
resource = resource.with({ | ||
fragment: Uri.parse(normalizedPath).fragment | ||
}); | ||
|
||
return this._openerService.open(resource); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I first tried with openEditor, but it didn't worked. Hence used open service. |
||
}); | ||
} | ||
|
||
|
@@ -188,12 +221,72 @@ export class TerminalLinkHandler { | |
return TPromise.as(void 0); | ||
} | ||
|
||
const linkUrl = this.extractLinkUrl(link); | ||
// Open an editor if the path exists | ||
return pfs.fileExists(link).then(isFile => { | ||
return pfs.fileExists(linkUrl).then(isFile => { | ||
if (!isFile) { | ||
return null; | ||
} | ||
return link; | ||
}); | ||
} | ||
|
||
/** | ||
* Appends line number and column number to link if they exists. | ||
* @param link link to format, will become link#line_num,col_num. | ||
*/ | ||
private _formatLocalLinkPath(link: string): string { | ||
const lineColumnInfo: LineColumnInfo = this.extractLineColumnInfo(link); | ||
if (lineColumnInfo.lineNumber) { | ||
link += `#${lineColumnInfo.lineNumber}`; | ||
|
||
if (lineColumnInfo.columnNumber) { | ||
link += `,${lineColumnInfo.columnNumber}`; | ||
} | ||
} | ||
|
||
return link; | ||
} | ||
|
||
/** | ||
* Returns line and column number of URl if that is present. | ||
* | ||
* @param link Url link which may contain line and column number. | ||
*/ | ||
public extractLineColumnInfo(link: string): LineColumnInfo { | ||
const matches: string[] = this._localLinkRegex.exec(link); | ||
const lineColumnInfo: LineColumnInfo = {}; | ||
const lineAndColumnMatchIndex = this._platform === platform.Platform.Windows ? winLineAndColumnMatchIndex : unixLineAndColumnMatchIndex; | ||
|
||
for (let i = 0; i < lineAndColumnClause.length; i++) { | ||
const lineMatchIndex = lineAndColumnMatchIndex + (lineAndColumnClauseGroupCount * i); | ||
const rowNumber = matches[lineMatchIndex]; | ||
if (rowNumber) { | ||
lineColumnInfo['lineNumber'] = rowNumber; | ||
// Check if column number exists | ||
const columnNumber = matches[lineMatchIndex + 2]; | ||
if (columnNumber) { | ||
lineColumnInfo['columnNumber'] = columnNumber; | ||
} | ||
break; | ||
} | ||
} | ||
|
||
return lineColumnInfo; | ||
} | ||
|
||
/** | ||
* Returns url from link as link may contain line and column information. | ||
* | ||
* @param link url link which may contain line and column number. | ||
*/ | ||
public extractLinkUrl(link: string): string { | ||
const matches: string[] = this._localLinkRegex.exec(link); | ||
return matches[1]; | ||
} | ||
} | ||
|
||
export interface LineColumnInfo { | ||
lineNumber?: string; | ||
columnNumber?: string; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Match index 1 only matches first group of regex, but we need other groups as well which have line and column number information.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But
matchIndex
is still generally supported right? Custom link matchers need to be able to specify it still.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Tyriar , Yes for custom link matchers MatchIndex is supported, it will follow given matchIndex.