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
Add initial search editor implementation #85424
Changes from all commits
5a82eee
59f0f37
4190245
cddaf74
0e144a4
ae6672a
9bdf7de
1dd9bd3
7bca29a
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,3 @@ | ||
# Language Features for Search Result files | ||
|
||
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
{ | ||
"name": "search-result", | ||
"displayName": "%displayName%", | ||
"description": "%description%", | ||
"version": "1.0.0", | ||
"publisher": "vscode", | ||
"license": "MIT", | ||
"engines": { | ||
"vscode": "^1.39.0" | ||
}, | ||
"categories": [ | ||
"Programming Languages" | ||
], | ||
"main": "out/extension.js", | ||
"activationEvents": [ | ||
"*" | ||
], | ||
"contributes": { | ||
"languages": [ | ||
{ | ||
"id": "search-result", | ||
"extensions": [ | ||
".code-search" | ||
], | ||
"aliases": [ | ||
"Search Result" | ||
] | ||
} | ||
], | ||
"grammars": [ | ||
{ | ||
"language": "search-result", | ||
"scopeName": "text.searchResult", | ||
"path": "./syntaxes/searchResult.tmLanguage.json" | ||
} | ||
] | ||
}, | ||
"devDependencies": { | ||
"vscode": "^1.1.36" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"displayName": "Search Result", | ||
"description": "Provides syntax highlighting and language features for tabbed search results." | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import * as vscode from 'vscode'; | ||
import * as pathUtils from 'path'; | ||
|
||
const FILE_LINE_REGEX = /^(\S.*):$/; | ||
const RESULT_LINE_REGEX = /^(\s+)(\d+):(\s+)(.*)$/; | ||
|
||
let cachedLastParse: { version: number, parse: ParsedSearchResults } | undefined; | ||
|
||
export function activate() { | ||
|
||
vscode.languages.registerDefinitionProvider({ language: 'search-result' }, { | ||
provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): vscode.DefinitionLink[] { | ||
const lineResult = parseSearchResults(document, token)[position.line]; | ||
if (!lineResult) { return []; } | ||
if (lineResult.type === 'file') { | ||
return lineResult.allLocations.length > 1 ? lineResult.allLocations : [lineResult.location]; | ||
} | ||
|
||
return [lineResult.location]; | ||
} | ||
}); | ||
|
||
vscode.languages.registerDocumentLinkProvider({ language: 'search-result' }, { | ||
async provideDocumentLinks(document: vscode.TextDocument, token: vscode.CancellationToken): Promise<vscode.DocumentLink[]> { | ||
return parseSearchResults(document, token) | ||
.filter(({ type }) => type === 'file') | ||
.map(({ location }) => ({ range: location.originSelectionRange!, target: location.targetUri })); | ||
} | ||
}); | ||
|
||
vscode.window.onDidChangeActiveTextEditor(e => { | ||
if (e?.document.languageId === 'search-result') { | ||
// Clear the parse whenever we open a new editor. | ||
// Conservative because things like the URI might remain constant even if the contents change, and re-parsing even large files is relatively fast. | ||
cachedLastParse = undefined; | ||
} | ||
}); | ||
} | ||
|
||
|
||
function relativePathToUri(path: string, resultsUri: vscode.Uri): vscode.Uri | undefined { | ||
if (pathUtils.isAbsolute(path)) { return vscode.Uri.file(path); } | ||
if (path.indexOf('~/') === 0) { | ||
return vscode.Uri.file(pathUtils.join(process.env.HOME!, path.slice(2))); | ||
} | ||
|
||
|
||
if (vscode.workspace.workspaceFolders) { | ||
const multiRootFormattedPath = /^(.*) • (.*)$/.exec(path); | ||
if (multiRootFormattedPath) { | ||
const [, workspaceName, workspacePath] = multiRootFormattedPath; | ||
const folder = vscode.workspace.workspaceFolders.filter(wf => wf.name === workspaceName)[0]; | ||
if (folder) { | ||
return vscode.Uri.file(pathUtils.join(folder.uri.fsPath, workspacePath)); | ||
} | ||
} | ||
|
||
else if (vscode.workspace.workspaceFolders.length === 1) { | ||
return vscode.Uri.file(pathUtils.join(vscode.workspace.workspaceFolders[0].uri.fsPath, path)); | ||
} else if (resultsUri.scheme !== 'untitled') { | ||
// We're in a multi-root workspace, but the path is not multi-root formatted | ||
// Possibly a saved search from a single root session. Try checking if the search result document's URI is in a current workspace folder. | ||
const prefixMatch = vscode.workspace.workspaceFolders.filter(wf => resultsUri.toString().startsWith(wf.uri.toString()))[0]; | ||
if (prefixMatch) { return vscode.Uri.file(pathUtils.join(prefixMatch.uri.fsPath, path)); } | ||
} | ||
} | ||
|
||
console.error(`Unable to resolve path ${path}`); | ||
return undefined; | ||
} | ||
|
||
type ParsedSearchResults = Array< | ||
{ type: 'file', location: vscode.LocationLink, allLocations: vscode.LocationLink[] } | | ||
{ type: 'result', location: vscode.LocationLink } | ||
>; | ||
|
||
function parseSearchResults(document: vscode.TextDocument, token: vscode.CancellationToken): ParsedSearchResults { | ||
|
||
if (cachedLastParse && cachedLastParse.version === document.version) { | ||
return cachedLastParse.parse; | ||
} | ||
|
||
const lines = document.getText().split(/\r?\n/); | ||
const links: ParsedSearchResults = []; | ||
|
||
let currentTarget: vscode.Uri | undefined = undefined; | ||
let currentTargetLocations: vscode.LocationLink[] | undefined = undefined; | ||
|
||
for (let i = 0; i < lines.length; i++) { | ||
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. Will this be slow, especially if we increase the 10000 result limit? Maybe as an optimization later this could be done more lazily. 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. We have to iterate over the entire document anyways for the file name lines for the link provider, and by parsing the result lines too we can add them as targets to the file name definition provider. So for now I think this is fine, but if yeah if we increse the limit later that could be worth looking into. |
||
if (token.isCancellationRequested) { return []; } | ||
const line = lines[i]; | ||
|
||
const fileLine = FILE_LINE_REGEX.exec(line); | ||
if (fileLine) { | ||
const [, path] = fileLine; | ||
|
||
currentTarget = relativePathToUri(path, document.uri); | ||
if (!currentTarget) { continue; } | ||
currentTargetLocations = []; | ||
|
||
const location: vscode.LocationLink = { | ||
targetRange: new vscode.Range(0, 0, 0, 1), | ||
targetUri: currentTarget, | ||
originSelectionRange: new vscode.Range(i, 0, i, line.length), | ||
}; | ||
|
||
|
||
links[i] = { type: 'file', location, allLocations: currentTargetLocations }; | ||
} | ||
|
||
if (!currentTarget) { continue; } | ||
|
||
const resultLine = RESULT_LINE_REGEX.exec(line); | ||
if (resultLine) { | ||
const [, indentation, _lineNumber, resultIndentation] = resultLine; | ||
const lineNumber = +_lineNumber - 1; | ||
const resultStart = (indentation + _lineNumber + ':' + resultIndentation).length; | ||
|
||
const location: vscode.LocationLink = { | ||
targetRange: new vscode.Range(Math.max(lineNumber - 3, 0), 0, lineNumber + 3, line.length), | ||
targetSelectionRange: new vscode.Range(lineNumber, 0, lineNumber, line.length), | ||
targetUri: currentTarget, | ||
originSelectionRange: new vscode.Range(i, resultStart, i, line.length), | ||
}; | ||
|
||
currentTargetLocations?.push(location); | ||
|
||
links[i] = { type: 'result', location }; | ||
} | ||
} | ||
|
||
cachedLastParse = { | ||
version: document.version, | ||
parse: links | ||
}; | ||
|
||
return links; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
/// <reference path='../../../../src/vs/vscode.d.ts'/> | ||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"name": "Search Results", | ||
"scopeName": "text.searchResult", | ||
"patterns": [ | ||
{ | ||
"match": "^# (Query|Flags|Include|Exclude): .*$", | ||
"name": "comment" | ||
}, | ||
{ | ||
"match": "^\\S.*:$", | ||
"name": "string path.searchResult" | ||
}, | ||
{ | ||
"match": "^ \\d+", | ||
"name": "constant.numeric lineNumber.searchResult" | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"extends": "../shared.tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "./out", | ||
}, | ||
"include": [ | ||
"src/**/*" | ||
] | ||
} |
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.
These should probably be formatted with
\
on windowsThere 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.
The
~/
? That's a Mac/Linux-specific thing, thePath
's don't get prefixed on Windows.