Skip to content
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

Resolve links to workspace files in terminal #13498

Merged
merged 2 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> {
planger marked this conversation as resolved.
Show resolved Hide resolved
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
Loading