Skip to content

Commit

Permalink
Resolve links to workspace files in terminal (#13498)
Browse files Browse the repository at this point in the history
* Resolve links to workspace files in terminal by searching for matching files
* Match files without a starting separator, e.g. 'yarn.lock:25:2'

Contributed on behalf of STMicroelectronics
  • Loading branch information
AlexandraBuzila authored Mar 26, 2024
1 parent 8ea1846 commit ddb91b9
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 8 deletions.
1 change: 1 addition & 0 deletions packages/terminal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@theia/process": "1.47.0",
"@theia/variable-resolver": "1.47.0",
"@theia/workspace": "1.47.0",
"@theia/file-search": "1.47.0",
"tslib": "^2.6.2",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
Expand Down
73 changes: 66 additions & 7 deletions packages/terminal/src/browser/terminal-file-link-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { TerminalWidget } from './base/terminal-widget';
import { TerminalLink, TerminalLinkProvider } from './terminal-link-provider';
import { TerminalWidgetImpl } from './terminal-widget-impl';

import { FileSearchService } from '@theia/file-search/lib/common/file-search-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
@injectable()
export class FileLinkProvider implements TerminalLinkProvider {

@inject(OpenerService) protected readonly openerService: OpenerService;
@inject(FileService) protected fileService: FileService;
@inject(FileSearchService) protected searchService: FileSearchService;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;

async provideLinks(line: string, terminal: TerminalWidget): Promise<TerminalLink[]> {
const links: TerminalLink[] = [];
Expand All @@ -42,11 +45,48 @@ export class FileLinkProvider implements TerminalLinkProvider {
length: match.length,
handle: () => this.open(match, terminal)
});
} else {
const searchTerm = await this.extractPath(match);
const fileUri = await this.isValidWorkspaceFile(searchTerm, terminal);
if (fileUri) {
const position = await this.extractPosition(match);
links.push({
startIndex: regExp.lastIndex - match.length,
length: match.length,
handle: () => this.openURI(fileUri, position)
});
}
}
}
return links;
}

protected async isValidWorkspaceFile(searchTerm: string | undefined, terminal: TerminalWidget): Promise<URI | undefined> {
if (!searchTerm) {
return undefined;
}
const cwd = await this.getCwd(terminal);
// remove any leading ./, ../ etc. as they can't be searched
searchTerm = searchTerm.replace(/^(\.+[\\/])+/, '');
const workspaceRoots = this.workspaceService.tryGetRoots().map(root => root.resource.toString());
// try and find a matching file in the workspace
const files = (await this.searchService.find(searchTerm, {
rootUris: [cwd.toString(), ...workspaceRoots],
fuzzyMatch: true,
limit: 1
}));
// checks if the string end in a separator + searchTerm
const regex = new RegExp(`[\\\\|\\/]${searchTerm}$`);
if (files.length && regex.test(files[0])) {
const fileUri = new URI(files[0]);
const valid = await this.isValidFileURI(fileUri);
if (valid) {
return fileUri;
}

}
}

protected async createRegExp(): Promise<RegExp> {
const baseLocalLinkClause = OS.backend.isWindows ? winLocalLinkClause : unixLocalLinkClause;
return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`, 'g');
Expand All @@ -57,17 +97,22 @@ export class FileLinkProvider implements TerminalLinkProvider {
const toOpen = await this.toURI(match, await this.getCwd(terminal));
if (toOpen) {
// TODO: would be better to ask the opener service, but it returns positively even for unknown files.
try {
const stat = await this.fileService.resolve(toOpen);
return !stat.isDirectory;
} catch { }
return this.isValidFileURI(toOpen);
}
} catch (err) {
console.trace('Error validating ' + match, err);
}
return false;
}

protected async isValidFileURI(uri: URI): Promise<boolean> {
try {
const stat = await this.fileService.resolve(uri);
return !stat.isDirectory;
} catch { }
return false;
}

protected async toURI(match: string, cwd: URI): Promise<URI | undefined> {
const path = await this.extractPath(match);
if (!path) {
Expand Down Expand Up @@ -97,8 +142,11 @@ export class FileLinkProvider implements TerminalLinkProvider {
if (!toOpen) {
return;
}

const position = await this.extractPosition(match);
return this.openURI(toOpen, position);
}

async openURI(toOpen: URI, position: Position): Promise<void> {
let options = {};
if (position) {
options = { selection: { start: position } };
Expand All @@ -108,7 +156,7 @@ export class FileLinkProvider implements TerminalLinkProvider {
const opener = await this.openerService.getOpener(toOpen, options);
opener.open(toOpen, options);
} catch (err) {
console.error('Cannot open link ' + match, err);
console.error('Cannot open link ' + toOpen, err);
}
}

Expand Down Expand Up @@ -153,6 +201,17 @@ export class FileDiffPostLinkProvider extends FileLinkProvider {
}
}

@injectable()
export class LocalFileLinkProvider extends FileLinkProvider {
override async createRegExp(): Promise<RegExp> {
// match links that might not start with a separator, e.g. 'foo.bar'
const baseLocalLinkClause = OS.backend.isWindows ?
'((' + winPathPrefix + '|(' + winExcludedPathCharactersClause + ')+)(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)*)'
: '((' + pathPrefix + '|(' + excludedPathCharactersClause + ')+)(' + pathSeparatorClause + '(' + excludedPathCharactersClause + ')+)*)';
return new RegExp(`${baseLocalLinkClause}(${lineAndColumnClause})`, 'g');
}
}

// The following regular expressions are taken from:
// https://github.com/microsoft/vscode/blob/b118105bf28d773fbbce683f7230d058be2f89a7/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts#L34-L58

Expand Down
4 changes: 3 additions & 1 deletion packages/terminal/src/browser/terminal-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { TerminalThemeService } from './terminal-theme-service';
import { QuickAccessContribution } from '@theia/core/lib/browser/quick-input/quick-access';
import { createXtermLinkFactory, TerminalLinkProvider, TerminalLinkProviderContribution, XtermLinkFactory } from './terminal-link-provider';
import { UrlLinkProvider } from './terminal-url-link-provider';
import { FileDiffPostLinkProvider, FileDiffPreLinkProvider, FileLinkProvider } from './terminal-file-link-provider';
import { FileDiffPostLinkProvider, FileDiffPreLinkProvider, FileLinkProvider, LocalFileLinkProvider } from './terminal-file-link-provider';
import {
ContributedTerminalProfileStore, DefaultProfileStore, DefaultTerminalProfileService,
TerminalProfileService, TerminalProfileStore, UserTerminalProfileStore
Expand Down Expand Up @@ -123,6 +123,8 @@ export default new ContainerModule(bind => {
bind(TerminalLinkProvider).toService(FileDiffPreLinkProvider);
bind(FileDiffPostLinkProvider).toSelf().inSingletonScope();
bind(TerminalLinkProvider).toService(FileDiffPostLinkProvider);
bind(LocalFileLinkProvider).toSelf().inSingletonScope();
bind(TerminalLinkProvider).toService(LocalFileLinkProvider);

bind(ContributedTerminalProfileStore).to(DefaultProfileStore).inSingletonScope();
bind(UserTerminalProfileStore).to(DefaultProfileStore).inSingletonScope();
Expand Down
3 changes: 3 additions & 0 deletions packages/terminal/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
{
"path": "../editor"
},
{
"path": "../file-search"
},
{
"path": "../filesystem"
},
Expand Down

0 comments on commit ddb91b9

Please sign in to comment.