Skip to content
This repository was archived by the owner on Sep 9, 2022. It is now read-only.

Commit

Permalink
feat: open links in terminal with alt+click
Browse files Browse the repository at this point in the history
  • Loading branch information
xcodebuild committed May 9, 2022
1 parent 24303df commit af587b4
Show file tree
Hide file tree
Showing 2 changed files with 16 additions and 117 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class TerminalLinkManager extends DisposableStore {
this._openers.set(TerminalBuiltinLinkType.LocalFile, localFileOpener);
this._openers.set(TerminalBuiltinLinkType.LocalFolderInWorkspace, localFolderInWorkspaceOpener);
this._openers.set(TerminalBuiltinLinkType.LocalFolderOutsideWorkspace, this._instantiationService.createInstance(TerminalLocalFolderOutsideWorkspaceLinkOpener));
this._openers.set(TerminalBuiltinLinkType.Search, this._instantiationService.createInstance(TerminalSearchLinkOpener, capabilities, localFileOpener, localFolderInWorkspaceOpener, this._processManager.os || OS));
this._openers.set(TerminalBuiltinLinkType.Search, this._instantiationService.createInstance(TerminalSearchLinkOpener));
this._openers.set(TerminalBuiltinLinkType.Url, this._instantiationService.createInstance(TerminalUrlLinkOpener, !!this._processManager.remoteAuthority));

this._registerStandardLinkProviders();
Expand Down Expand Up @@ -392,6 +392,8 @@ export class TerminalLinkManager extends DisposableStore {
return cached;
}

this._resolvedLinkCache.set(link, null);

if (uri) {
try {
const stat = await this._fileService.stat(uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,30 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Schemas } from 'vs/base/common/network';
import { OperatingSystem } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ITerminalLinkOpener, ITerminalSimpleLink } from 'vs/workbench/contrib/terminal/browser/links/links';
import { osPathModule, updateLinkWithRelativeCwd } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers';
import { ILineColumnInfo } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
import { getLocalLinkRegex, lineAndColumnClause, lineAndColumnClauseGroupCount, unixLineAndColumnMatchIndex, winLineAndColumnMatchIndex } from 'vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector';
import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder';
import { ISearchService } from 'vs/workbench/services/search/common/search';
import * as open from 'open';


async function openLink(link: ITerminalSimpleLink) {
if (!link.uri) {
throw new Error('Tried to open a url without a resolved URI');
}
await open(link.uri.toString());
}

export class TerminalLocalFileLinkOpener implements ITerminalLinkOpener {
constructor(
private readonly _os: OperatingSystem,
@IEditorService private readonly _editorService: IEditorService,
) {
}

async open(link: ITerminalSimpleLink): Promise<void> {
if (!link.uri) {
throw new Error('Tried to open file link without a resolved URI');
}
const lineColumnInfo: ILineColumnInfo = this.extractLineColumnInfo(link.text);
const selection: ITextEditorSelection = {
startLineNumber: lineColumnInfo.lineNumber,
startColumn: lineColumnInfo.columnNumber
};
await this._editorService.openEditor({
resource: link.uri,
options: { pinned: true, selection, revealIfOpened: true }
});
return await openLink(link);
}

/**
Expand Down Expand Up @@ -94,109 +77,23 @@ export class TerminalLocalFolderInWorkspaceLinkOpener implements ITerminalLinkOp
}

export class TerminalLocalFolderOutsideWorkspaceLinkOpener implements ITerminalLinkOpener {
constructor(@IHostService private readonly _hostService: IHostService) {
constructor() {
}

async open(link: ITerminalSimpleLink): Promise<void> {
if (!link.uri) {
throw new Error('Tried to open folder in workspace link without a resolved URI');
}
this._hostService.openWindow([{ folderUri: link.uri }], { forceNewWindow: true });
return await openLink(link);
}
}

export class TerminalSearchLinkOpener implements ITerminalLinkOpener {
private readonly _fileQueryBuilder = this._instantiationService.createInstance(QueryBuilder);

constructor(
private readonly _capabilities: ITerminalCapabilityStore,
private readonly _localFileOpener: TerminalLocalFileLinkOpener,
private readonly _localFolderInWorkspaceOpener: TerminalLocalFolderInWorkspaceLinkOpener,
private readonly _os: OperatingSystem,
@IFileService private readonly _fileService: IFileService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@ISearchService private readonly _searchService: ISearchService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IWorkbenchEnvironmentService private readonly _workbenchEnvironmentService: IWorkbenchEnvironmentService,
) {
}

async open(link: ITerminalSimpleLink): Promise<void> {
const pathSeparator = osPathModule(this._os).sep;
// Remove file:/// and any leading ./ or ../ since quick access doesn't understand that format
let text = link.text.replace(/^file:\/\/\/?/, '');
text = osPathModule(this._os).normalize(text).replace(/^(\.+[\\/])+/, '');

// Remove `:in` from the end which is how Ruby outputs stack traces
text = text.replace(/:in$/, '');
// If any of the names of the folders in the workspace matches
// a prefix of the link, remove that prefix and continue
this._workspaceContextService.getWorkspace().folders.forEach((folder) => {
if (text.substring(0, folder.name.length + 1) === folder.name + pathSeparator) {
text = text.substring(folder.name.length + 1);
return;
}
});
let matchLink = text;
if (this._capabilities.has(TerminalCapability.CommandDetection)) {
matchLink = updateLinkWithRelativeCwd(this._capabilities, link.bufferRange.start.y, text, pathSeparator) || text;
}
const sanitizedLink = matchLink.replace(/:\d+(:\d+)?$/, '');
try {
const result = await this._getExactMatch(sanitizedLink);
if (result) {
const { uri, isDirectory } = result;
const linkToOpen = {
// Use the absolute URI's path here so the optional line/col get detected
text: result.uri.fsPath + (matchLink.match(/:\d+(:\d+)?$/)?.[0] || ''),
uri,
bufferRange: link.bufferRange,
type: link.type
};
if (uri) {
return isDirectory ? this._localFolderInWorkspaceOpener.open(linkToOpen) : this._localFileOpener.open(linkToOpen);
}
}
} catch {
// Fallback to searching quick access
return this._quickInputService.quickAccess.show(text);
}
// Fallback to searching quick access
return this._quickInputService.quickAccess.show(text);
return await openLink(link);
}

private async _getExactMatch(sanitizedLink: string): Promise<IResourceMatch | undefined> {
let resourceMatch: IResourceMatch | undefined;
if (osPathModule(this._os).isAbsolute(sanitizedLink)) {
const scheme = this._workbenchEnvironmentService.remoteAuthority ? Schemas.vscodeRemote : Schemas.file;
const uri = URI.from({ scheme, path: sanitizedLink });
try {
const fileStat = await this._fileService.stat(uri);
resourceMatch = { uri, isDirectory: fileStat.isDirectory };
} catch {
// File or dir doesn't exist, continue on
}
}
if (!resourceMatch) {
const results = await this._searchService.fileSearch(
this._fileQueryBuilder.file(this._workspaceContextService.getWorkspace().folders, {
// Remove optional :row:col from the link as openEditor supports it
filePattern: sanitizedLink,
maxResults: 2
})
);
if (results.results.length === 1) {
resourceMatch = { uri: results.results[0].resource };
}
}
return resourceMatch;
}
}

interface IResourceMatch {
uri: URI;
isDirectory?: boolean;
}

export class TerminalUrlLinkOpener implements ITerminalLinkOpener {
Expand Down

0 comments on commit af587b4

Please # to comment.