-
Notifications
You must be signed in to change notification settings - Fork 30.2k
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
Link detection for the exception widget and debug console #24451
Changes from all commits
b32837c
353eec9
dd96b88
c09c79f
0337cfd
e793d3e
583fe76
37241e6
5f87c7b
a6d3ae2
4962cfe
9901d2b
df6122e
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 |
---|---|---|
@@ -0,0 +1,113 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import strings = require('vs/base/common/strings'); | ||
import uri from 'vs/base/common/uri'; | ||
import { isMacintosh } from 'vs/base/common/platform'; | ||
import * as errors from 'vs/base/common/errors'; | ||
import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; | ||
import * as nls from 'vs/nls'; | ||
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; | ||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; | ||
|
||
export class LinkDetector { | ||
private static FILE_LOCATION_PATTERNS: RegExp[] = [ | ||
// group 0: full path with line and column | ||
// group 1: full path without line and column, matched by `*.*` in the end to work only on paths with extensions in the end (s.t. node:10352 would not match) | ||
// group 2: drive letter on windows with trailing backslash or leading slash on mac/linux | ||
// group 3: line number, matched by (:(\d+)) | ||
// group 4: column number, matched by ((?::(\d+))?) | ||
// eg: at Context.<anonymous> (c:\Users\someone\Desktop\mocha-runner\test\test.js:26:11) | ||
/(?![\(])(?:file:\/\/)?((?:([a-zA-Z]+:)|[^\(\)<>\'\"\[\]:\s]+)(?:[\\/][^\(\)<>\'\"\[\]:]*)?\.[a-zA-Z]+[0-9]*):(\d+)(?::(\d+))?/g | ||
]; | ||
|
||
constructor( | ||
@IWorkbenchEditorService private editorService: IWorkbenchEditorService, | ||
@IWorkspaceContextService private contextService: IWorkspaceContextService | ||
) { | ||
// noop | ||
} | ||
|
||
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. Since this For ideas you can also check what does the terminal / editor link detector do and how does their API look like 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. @isidorn I've added the comment to it. I hope this explains it well. If you have any suggestions on what should be a return result instead, I am happy to hear. Currently it perfectly suits exception widget and debug console needs. |
||
/** | ||
* Matches and handles relative and absolute file links in the string provided. | ||
* Returns <span/> element that wraps the processed string, where matched links are replaced by <a/> and unmatched parts are surrounded by <span/> elements. | ||
* 'onclick' event is attached to all anchored links that opens them in the editor. | ||
* If no links were detected, returns the original string. | ||
*/ | ||
public handleLinks(text: string): HTMLElement | string { | ||
let linkContainer: HTMLElement; | ||
|
||
for (let pattern of LinkDetector.FILE_LOCATION_PATTERNS) { | ||
pattern.lastIndex = 0; // the holy grail of software development | ||
let lastMatchIndex = 0; | ||
|
||
let match = pattern.exec(text); | ||
while (match !== null) { | ||
let resource: uri = null; | ||
try { | ||
resource = (match && !strings.startsWith(match[0], 'http')) && (match[2] ? uri.file(match[1]) : this.contextService.toResource(match[1])); | ||
} catch (e) { } | ||
|
||
if (!resource) { | ||
match = pattern.exec(text); | ||
continue; | ||
} | ||
if (!linkContainer) { | ||
linkContainer = document.createElement('span'); | ||
} | ||
|
||
let textBeforeLink = text.substring(lastMatchIndex, match.index); | ||
if (textBeforeLink) { | ||
let span = document.createElement('span'); | ||
span.textContent = textBeforeLink; | ||
linkContainer.appendChild(span); | ||
} | ||
|
||
const link = document.createElement('a'); | ||
link.textContent = text.substr(match.index, match[0].length); | ||
link.title = isMacintosh ? nls.localize('fileLinkMac', "Click to follow (Cmd + click opens to the side)") : nls.localize('fileLink', "Click to follow (Ctrl + click opens to the side)"); | ||
linkContainer.appendChild(link); | ||
const line = Number(match[3]); | ||
const column = match[4] ? Number(match[4]) : undefined; | ||
link.onclick = (e) => this.onLinkClick(new StandardMouseEvent(e), resource, line, column); | ||
|
||
lastMatchIndex = pattern.lastIndex; | ||
const currentMatch = match; | ||
match = pattern.exec(text); | ||
|
||
// Append last string part if no more link matches | ||
if (!match) { | ||
let textAfterLink = text.substr(currentMatch.index + currentMatch[0].length); | ||
if (textAfterLink) { | ||
let span = document.createElement('span'); | ||
span.textContent = textAfterLink; | ||
linkContainer.appendChild(span); | ||
} | ||
} | ||
} | ||
} | ||
|
||
return linkContainer || text; | ||
} | ||
|
||
private onLinkClick(event: IMouseEvent, resource: uri, line: number, column: number = 0): void { | ||
const selection = window.getSelection(); | ||
if (selection.type === 'Range') { | ||
return; // do not navigate when user is selecting | ||
} | ||
|
||
event.preventDefault(); | ||
|
||
this.editorService.openEditor({ | ||
resource, | ||
options: { | ||
selection: { | ||
startLineNumber: line, | ||
startColumn: column | ||
} | ||
} | ||
}, event.ctrlKey || event.metaKey).done(null, errors.onUnexpectedError); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,11 @@ | |
margin-top: 0.5em; | ||
} | ||
|
||
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. Makes sense |
||
.monaco-editor .zone-widget .zone-widget-container.exception-widget a { | ||
text-decoration: underline; | ||
cursor: pointer; | ||
} | ||
|
||
/* High Contrast Theming */ | ||
|
||
.monaco-workbench.mac .zone-widget .zone-widget-container.exception-widget { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,13 +7,10 @@ import * as nls from 'vs/nls'; | |
import { TPromise } from 'vs/base/common/winjs.base'; | ||
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. Looks good overall and I like we removed code from this class. I only have the issue that you are creating 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. @isidorn You're right, did not see that for-loop when refactored. I've moved LinkDetector instantiation to the constructor. |
||
import { IAction } from 'vs/base/common/actions'; | ||
import { isFullWidthCharacter, removeAnsiEscapeCodes, endsWith } from 'vs/base/common/strings'; | ||
import uri from 'vs/base/common/uri'; | ||
import { isMacintosh } from 'vs/base/common/platform'; | ||
import { IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; | ||
import * as dom from 'vs/base/browser/dom'; | ||
import * as errors from 'vs/base/common/errors'; | ||
import severity from 'vs/base/common/severity'; | ||
import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; | ||
import { IMouseEvent } from 'vs/base/browser/mouseEvent'; | ||
import { ITree, IAccessibilityProvider, IDataSource, IRenderer, IActionProvider } from 'vs/base/parts/tree/browser/tree'; | ||
import { ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults'; | ||
import { IExpressionContainer, IExpression } from 'vs/workbench/parts/debug/common/debug'; | ||
|
@@ -23,6 +20,7 @@ import { ClearReplAction } from 'vs/workbench/parts/debug/browser/debugActions'; | |
import { CopyAction } from 'vs/workbench/parts/debug/electron-browser/electronDebugActions'; | ||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; | ||
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; | ||
import { LinkDetector } from 'vs/workbench/parts/debug/browser/linkDetector'; | ||
|
||
const $ = dom.$; | ||
|
||
|
@@ -83,25 +81,18 @@ export class ReplExpressionsRenderer implements IRenderer { | |
private static VALUE_OUTPUT_TEMPLATE_ID = 'outputValue'; | ||
private static NAME_VALUE_OUTPUT_TEMPLATE_ID = 'outputNameValue'; | ||
|
||
private static FILE_LOCATION_PATTERNS: RegExp[] = [ | ||
// group 0: the full thing :) | ||
// group 1: absolute path | ||
// group 2: drive letter on windows with trailing backslash or leading slash on mac/linux | ||
// group 3: line number | ||
// group 4: column number | ||
// eg: at Context.<anonymous> (c:\Users\someone\Desktop\mocha-runner\test\test.js:26:11) | ||
/((\/|[a-zA-Z]:\\)[^\(\)<>\'\"\[\]]+):(\d+):(\d+)/ | ||
]; | ||
|
||
private static LINE_HEIGHT_PX = 18; | ||
|
||
private width: number; | ||
private characterWidth: number; | ||
|
||
private linkDetector: LinkDetector; | ||
|
||
constructor( | ||
@IWorkbenchEditorService private editorService: IWorkbenchEditorService | ||
@IWorkbenchEditorService private editorService: IWorkbenchEditorService, | ||
@IInstantiationService private instantiationService: IInstantiationService | ||
) { | ||
// noop | ||
this.linkDetector = this.instantiationService.createInstance(LinkDetector); | ||
} | ||
|
||
public getHeight(tree: ITree, element: any): number { | ||
|
@@ -330,7 +321,7 @@ export class ReplExpressionsRenderer implements IRenderer { | |
|
||
// flush text buffer if we have any | ||
if (buffer) { | ||
this.insert(this.handleLinks(buffer), currentToken || tokensContainer); | ||
this.insert(this.linkDetector.handleLinks(buffer), currentToken || tokensContainer); | ||
buffer = ''; | ||
} | ||
|
||
|
@@ -350,7 +341,7 @@ export class ReplExpressionsRenderer implements IRenderer { | |
|
||
// flush remaining text buffer if we have any | ||
if (buffer) { | ||
let res = this.handleLinks(buffer); | ||
let res = this.linkDetector.handleLinks(buffer); | ||
if (typeof res !== 'string' || currentToken) { | ||
if (!tokensContainer) { | ||
tokensContainer = document.createElement('span'); | ||
|
@@ -371,67 +362,6 @@ export class ReplExpressionsRenderer implements IRenderer { | |
} | ||
} | ||
|
||
private handleLinks(text: string): HTMLElement | string { | ||
let linkContainer: HTMLElement; | ||
|
||
for (let pattern of ReplExpressionsRenderer.FILE_LOCATION_PATTERNS) { | ||
pattern.lastIndex = 0; // the holy grail of software development | ||
|
||
const match = pattern.exec(text); | ||
let resource: uri = null; | ||
try { | ||
resource = match && uri.file(match[1]); | ||
} catch (e) { } | ||
|
||
if (resource) { | ||
linkContainer = document.createElement('span'); | ||
|
||
let textBeforeLink = text.substr(0, match.index); | ||
if (textBeforeLink) { | ||
let span = document.createElement('span'); | ||
span.textContent = textBeforeLink; | ||
linkContainer.appendChild(span); | ||
} | ||
|
||
const link = document.createElement('a'); | ||
link.textContent = text.substr(match.index, match[0].length); | ||
link.title = isMacintosh ? nls.localize('fileLinkMac', "Click to follow (Cmd + click opens to the side)") : nls.localize('fileLink', "Click to follow (Ctrl + click opens to the side)"); | ||
linkContainer.appendChild(link); | ||
link.onclick = (e) => this.onLinkClick(new StandardMouseEvent(e), resource, Number(match[3]), Number(match[4])); | ||
|
||
let textAfterLink = text.substr(match.index + match[0].length); | ||
if (textAfterLink) { | ||
let span = document.createElement('span'); | ||
span.textContent = textAfterLink; | ||
linkContainer.appendChild(span); | ||
} | ||
|
||
break; // support one link per line for now | ||
} | ||
} | ||
|
||
return linkContainer || text; | ||
} | ||
|
||
private onLinkClick(event: IMouseEvent, resource: uri, line: number, column: number): void { | ||
const selection = window.getSelection(); | ||
if (selection.type === 'Range') { | ||
return; // do not navigate when user is selecting | ||
} | ||
|
||
event.preventDefault(); | ||
|
||
this.editorService.openEditor({ | ||
resource, | ||
options: { | ||
selection: { | ||
startLineNumber: line, | ||
startColumn: column | ||
} | ||
} | ||
}, event.ctrlKey || event.metaKey).done(null, errors.onUnexpectedError); | ||
} | ||
|
||
public disposeTemplate(tree: ITree, templateId: string, templateData: any): void { | ||
// noop | ||
} | ||
|
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.
looks good