diff --git a/.prettierignore b/.prettierignore index 4fbb5a0a7..e7d0610c2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,4 +2,5 @@ lib dist out node_modules -**/src/vs/** +vscode-web-github1s/src/vs +vscode-web-github1s/extensions diff --git a/README.md b/README.md index 4b712e470..308dc7e05 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,10 @@ The continued development and maintenance of GitHub1s is made possible by these - [github-code-viewer](https://microsoftedge.microsoft.com/addons/detail/githubcodeviewer/jaaaapanahkknbgdbglnlchbjfhhjlpi) ([febaoshan/edge-extensions-github-code-viewer](https://github.com/febaoshan/edge-extensions-github-code-viewer)) - [Github Web IDE](https://microsoftedge.microsoft.com/addons/detail/akjbkjciknacicbnkfjbnlaeednpadcf) ([zvizvi/Github-Web-IDE](https://github.com/zvizvi/Github-Web-IDE)) +### Safari Extension + +- [GitHub1s-For-Safari-Extension](https://apps.apple.com/us/app/readcodeonline/id1569026520?mt=12) ([code4you2021/GitHub1s-For-Safari-Extension](https://github.com/code4you2021/GitHub1s-For-Safari-Extension)) + ### Tampermonkey scripts - [Mr-B0b/TamperMonkeyScripts/vscode.js](https://github.com/Mr-B0b/TamperMonkeyScripts/blob/main/vscode.js) diff --git a/extensions/github1s/src/extension.ts b/extensions/github1s/src/extension.ts index 282e48983..72de61822 100644 --- a/extensions/github1s/src/extension.ts +++ b/extensions/github1s/src/extension.ts @@ -17,7 +17,7 @@ import { registerEventListeners } from '@/listeners'; import { PageType } from './router/types'; export async function activate(context: vscode.ExtensionContext) { - const browserUrl = (await await vscode.commands.executeCommand( + const browserUrl = (await vscode.commands.executeCommand( 'github1s.vscode.get-browser-url' )) as string; @@ -40,17 +40,37 @@ export async function activate(context: vscode.ExtensionContext) { // sponsors in Status Bar showSponsors(); - await showGitpod(); + showGitpod(); - // open corresponding editor if there is a filePath specified in browser url - const { filePath, pageType } = await router.getState(); - if (filePath && [PageType.TREE, PageType.BLOB].includes(pageType)) { + // initialize the VSCode's state + initialVSCodeState(); +} + +// initialize the VSCode's state according to the router url +const initialVSCodeState = async () => { + const routerState = await router.getState(); + const { filePath, pageType } = routerState; + const scheme = GitHub1sFileSystemProvider.scheme; + + if (filePath && pageType === PageType.TREE) { vscode.commands.executeCommand( - pageType === PageType.TREE ? 'revealInExplorer' : 'vscode.open', - vscode.Uri.parse('').with({ - scheme: GitHub1sFileSystemProvider.scheme, - path: filePath, - }) + 'revealInExplorer', + vscode.Uri.parse('').with({ scheme, path: filePath }) + ); + } else if (filePath && pageType === PageType.BLOB) { + const { startLineNumber, endLineNumber } = routerState; + const start = new vscode.Position(startLineNumber - 1, 0); + const end = new vscode.Position(endLineNumber - 1, 999999); + const documentShowOptions: vscode.TextDocumentShowOptions = startLineNumber + ? { selection: new vscode.Range(start, end) } + : {}; + + // TODO: the selection of the opening file may be cleared + // when editor try to restore previous state in the same file + vscode.commands.executeCommand( + 'vscode.open', + vscode.Uri.parse('').with({ scheme, path: filePath }), + documentShowOptions ); } else if (pageType === PageType.PULL_LIST) { vscode.commands.executeCommand('github1s.views.pull-request-list.focus'); @@ -59,4 +79,4 @@ export async function activate(context: vscode.ExtensionContext) { } else if ([PageType.PULL, PageType.COMMIT].includes(pageType)) { vscode.commands.executeCommand('workbench.scm.focus'); } -} +}; diff --git a/extensions/github1s/src/helpers/func.ts b/extensions/github1s/src/helpers/func.ts index 3327221b3..d60eeda2e 100644 --- a/extensions/github1s/src/helpers/func.ts +++ b/extensions/github1s/src/helpers/func.ts @@ -41,3 +41,31 @@ export const throttle = any>( timer = setTimeout(() => (timer = null), interval); }; }; + +export const debounce = any>( + func: T, + wait: number +) => { + let timer = null; + return function (...args: Parameters): void { + timer && clearTimeout(timer); + timer = setTimeout(() => func.call(this, ...args), timer); + }; +}; + +// debounce an async func. once an async func canceled, it throws a exception +export const debounceAsyncFunc = Promise>( + func: T, + wait: number +) => { + let timer = null; + let previousReject = null; + return function (...args: Parameters): ReturnType { + return new Promise((resolve, reject) => { + timer && clearTimeout(timer); + previousReject && previousReject(); + timer = setTimeout(() => resolve(func.call(this, ...args)), wait); + previousReject = reject; + }) as ReturnType; + }; +}; diff --git a/extensions/github1s/src/helpers/urls.ts b/extensions/github1s/src/helpers/urls.ts new file mode 100644 index 000000000..4bfd19d28 --- /dev/null +++ b/extensions/github1s/src/helpers/urls.ts @@ -0,0 +1,16 @@ +/** + * @file extension url helpers + * @author netcon + */ + +export const getSourcegraphUrl = ( + owner: string, + repo: string, + ref: string, + path: string, + line: number, + character: number +): string => { + const repoUrl = `https://sourcegraph.com/github.com/${owner}/${repo}@${ref}`; + return `${repoUrl}/-/blob${path}#L${line + 1}:${character + 1}`; +}; diff --git a/extensions/github1s/src/interfaces/sourcegraph/common.ts b/extensions/github1s/src/interfaces/sourcegraph/common.ts new file mode 100644 index 000000000..f75ace07b --- /dev/null +++ b/extensions/github1s/src/interfaces/sourcegraph/common.ts @@ -0,0 +1,65 @@ +/** + * @file Sourcegraph api common utils + * @author netcon + */ + +import { + ApolloClient, + createHttpLink, + InMemoryCache, +} from '@apollo/client/core'; +import { trimEnd, trimStart } from '@/helpers/util'; + +const sourcegraphLink = createHttpLink({ + // Since the Sourcegraph refused the CORS check now, + // use Vercel Serverless Function to proxy it temporarily + // See `/api/sourcegraph.js` + uri: '/api/sourcegraph', +}); + +export const sourcegraphClient = new ApolloClient({ + link: sourcegraphLink, + cache: new InMemoryCache(), +}); + +export const canBeConvertToRegExp = (str: string) => { + try { + new RegExp(str); + return true; + } catch (e) { + return false; + } +}; + +export const combineGlobsToRegExp = (globs: string[]) => { + // only support very simple globs convert now + const result = Array.from( + new Set( + globs.map((glob: string) => + trimEnd(trimStart(glob, '*/'), '*/').replace(/^\./, '\\.') + ) + ) + ) + // if the glob still not can be convert to a regexp, just ignore it + .filter((item) => canBeConvertToRegExp(item)) + .join('|'); + // ensure the result can be convert to a regexp + return canBeConvertToRegExp(result) ? result : ''; +}; + +export const escapeRegexp = (text: string): string => + text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + +export const getRepoRefQueryString = ( + owner: string, + repo: string, + ref: string +) => { + // the string may looks like `^github\.com/conwnet/github1s$` + const repoPattern = `^${escapeRegexp(`github\.com/${owner}/${repo}`)}$`; + const repoRefQueryString = + ref.toUpperCase() === 'HEAD' + ? `repo:${repoPattern}` + : `repo:${repoPattern}@${ref}`; + return repoRefQueryString; +}; diff --git a/extensions/github1s/src/interfaces/sourcegraph/definition.ts b/extensions/github1s/src/interfaces/sourcegraph/definition.ts new file mode 100644 index 000000000..2e70332de --- /dev/null +++ b/extensions/github1s/src/interfaces/sourcegraph/definition.ts @@ -0,0 +1,137 @@ +/** + * @file Sourcegraph definition api + * @author netcon + */ + +import { gql } from '@apollo/client/core'; +import { sourcegraphClient } from './common'; +import { getSymbolPositions } from './position'; + +export interface SymbolDefinition { + precise: boolean; + owner: string; + repo: string; + ref: string; + path: string; + range: { + start: { + line: number; + character: number; + }; + end: { + line: number; + character: number; + }; + }; +} + +const LSIFDefinitionsQuery = gql` + query( + $repository: String! + $ref: String! + $path: String! + $line: Int! + $character: Int! + ) { + repository(name: $repository) { + commit(rev: $ref) { + blob(path: $path) { + lsif { + definitions(line: $line, character: $character) { + nodes { + resource { + path + repository { + name + } + commit { + oid + } + } + range { + start { + line + character + } + end { + line + character + } + } + } + } + } + } + } + } + } +`; + +// find definitions with Sourcegraph LSIF +// https://docs.sourcegraph.com/code_intelligence/explanations/precise_code_intelligence +const getLSIFDefinitions = async ( + owner: string, + repo: string, + ref: string, + path: string, + line: number, + character: number +): Promise => { + const response = await sourcegraphClient.query({ + query: LSIFDefinitionsQuery, + variables: { + repository: `github.com/${owner}/${repo}`, + ref, + path: path.slice(1), + line, + character, + }, + }); + const definitionNodes = + response?.data?.repository?.commit?.blob?.lsif?.definitions?.nodes; + return (definitionNodes || []).map(({ resource, range }) => { + const [owner, repo] = resource.repository.name + .split('/') + .filter(Boolean) + .slice(-2); + return { + precise: true, + owner, + repo, + ref: resource.commit.oid, + path: `/${resource.path}`, + range, + }; + }); +}; + +export const getSymbolDefinitions = ( + owner: string, + repo: string, + ref: string, + path: string, + line: number, + character: number, + symbol: string +): Promise => { + // if failed to find definitions from LSIF, + // fallback to search-based definitions, using + // two promise instead of `await` to request in + // parallel for getting result as soon as possible + const LSIFDefinitionsPromise = getLSIFDefinitions( + owner, + repo, + ref, + path, + line, + character + ); + const searchDefinitionsPromise = getSymbolPositions(owner, repo, ref, symbol); + + return LSIFDefinitionsPromise.then((LSIFDefinitions) => { + if (LSIFDefinitions.length) { + return LSIFDefinitions; + } + return searchDefinitionsPromise as Promise; + }); +}; diff --git a/extensions/github1s/src/interfaces/sourcegraph/hover.ts b/extensions/github1s/src/interfaces/sourcegraph/hover.ts new file mode 100644 index 000000000..d106308a8 --- /dev/null +++ b/extensions/github1s/src/interfaces/sourcegraph/hover.ts @@ -0,0 +1,77 @@ +/** + * @file Sourcegraph hover api + * @author netcon + */ + +import { gql } from '@apollo/client/core'; +import { sourcegraphClient } from './common'; + +export interface SymbolHover { + precise: boolean; + markdown: string; +} + +const LSIFHoverQuery = gql` + query( + $repository: String! + $ref: String! + $path: String! + $line: Int! + $character: Int! + ) { + repository(name: $repository) { + commit(rev: $ref) { + blob(path: $path) { + lsif { + hover(line: $line, character: $character) { + markdown { + text + } + } + } + } + } + } + } +`; + +// find Hover with Sourcegraph LSIF +// https://docs.sourcegraph.com/code_intelligence/explanations/precise_code_intelligence +const getLSIFHover = async ( + owner: string, + repo: string, + ref: string, + path: string, + line: number, + character: number +): Promise => { + const response = await sourcegraphClient.query({ + query: LSIFHoverQuery, + variables: { + repository: `github.com/${owner}/${repo}`, + ref, + path: path.slice(1), + line, + character, + }, + }); + const markdown = + response?.data?.repository?.commit?.blob?.lsif?.hover?.markdown?.text; + + if (!markdown) { + return null; + } + + return { precise: true, markdown }; +}; + +export const getSymbolHover = ( + owner: string, + repo: string, + ref: string, + path: string, + line: number, + character: number +): Promise => { + return getLSIFHover(owner, repo, ref, path, line, character); +}; diff --git a/extensions/github1s/src/interfaces/sourcegraph/position.ts b/extensions/github1s/src/interfaces/sourcegraph/position.ts new file mode 100644 index 000000000..a568f5eba --- /dev/null +++ b/extensions/github1s/src/interfaces/sourcegraph/position.ts @@ -0,0 +1,109 @@ +/** + * @file Sourcegraph api for searching symbol positions + * @author netcon + */ + +import { gql } from '@apollo/client/core'; +import { + escapeRegexp, + sourcegraphClient, + getRepoRefQueryString, +} from './common'; + +export interface SymbolPosition { + precise: boolean; + owner: string; + repo: string; + ref: string; + path: string; + range: { + start: { + line: number; + character: number; + }; + end: { + line: number; + character: number; + }; + }; +} + +const searchPositionsQuery = gql` + query($query: String!) { + search(query: $query) { + results { + results { + ... on FileMatch { + symbols { + name + kind + location { + resource { + path + repository { + name + } + commit { + oid + } + } + range { + start { + line + character + } + end { + line + character + } + } + } + } + } + } + } + } + } +`; + +// get symbol position information base on search, +// used by definition, reference and hover +export const getSymbolPositions = async ( + owner: string, + repo: string, + ref: string, + symbol: string +): Promise => { + const repoRefString = getRepoRefQueryString(owner, repo, ref); + const optionsString = [ + 'context:global', + 'type:symbol', + 'patternType:regexp', + 'case:yes', + ].join(' '); + const patternString = `^${escapeRegexp(symbol)}$`; + const query = [repoRefString, optionsString, patternString].join(' '); + const response = await sourcegraphClient.query({ + query: searchPositionsQuery, + variables: { query }, + }); + + const resultSymbols = response?.data?.search?.results?.results?.flatMap( + (item) => item.symbols + ); + return (resultSymbols || []).map((symbol) => { + const { resource, range } = symbol.location; + const [owner, repo] = resource.repository.name + .split('/') + .filter(Boolean) + .slice(-2); + return { + precise: false, + owner, + repo, + ref: resource.commit.oid, + path: `/${resource.path}`, + range, + }; + }); +}; diff --git a/extensions/github1s/src/interfaces/sourcegraph/reference.ts b/extensions/github1s/src/interfaces/sourcegraph/reference.ts new file mode 100644 index 000000000..cf9054aa7 --- /dev/null +++ b/extensions/github1s/src/interfaces/sourcegraph/reference.ts @@ -0,0 +1,137 @@ +/** + * @file Sourcegraph reference api + * @author netcon + */ + +import { gql } from '@apollo/client/core'; +import { sourcegraphClient } from './common'; +import { getSymbolPositions } from './position'; + +export interface SymbolReference { + precise: boolean; + owner: string; + repo: string; + ref: string; + path: string; + range: { + start: { + line: number; + character: number; + }; + end: { + line: number; + character: number; + }; + }; +} + +const LSIFReferencesQuery = gql` + query( + $repository: String! + $ref: String! + $path: String! + $line: Int! + $character: Int! + ) { + repository(name: $repository) { + commit(rev: $ref) { + blob(path: $path) { + lsif { + references(line: $line, character: $character) { + nodes { + resource { + path + repository { + name + } + commit { + oid + } + } + range { + start { + line + character + } + end { + line + character + } + } + } + } + } + } + } + } + } +`; + +// find references with Sourcegraph LSIF +// https://docs.sourcegraph.com/code_intelligence/explanations/precise_code_intelligence +const getLSIFReferences = async ( + owner: string, + repo: string, + ref: string, + path: string, + line: number, + character: number +): Promise => { + const response = await sourcegraphClient.query({ + query: LSIFReferencesQuery, + variables: { + repository: `github.com/${owner}/${repo}`, + ref, + path: path.slice(1), + line, + character, + }, + }); + const referenceNodes = + response?.data?.repository?.commit?.blob?.lsif?.references?.nodes; + return (referenceNodes || []).map(({ resource, range }) => { + const [owner, repo] = resource.repository.name + .split('/') + .filter(Boolean) + .slice(-2); + return { + precise: true, + owner, + repo, + ref: resource.commit.oid, + path: `/${resource.path}`, + range, + }; + }); +}; + +export const getSymbolReferences = ( + owner: string, + repo: string, + ref: string, + path: string, + line: number, + character: number, + symbol: string +): Promise => { + // if failed to find references from LSIF, + // fallback to search-based references, using + // two promise instead of `await` to request in + // parallel for getting result as soon as possible + const LSIFReferencesPromise = getLSIFReferences( + owner, + repo, + ref, + path, + line, + character + ); + const searchReferencesPromise = getSymbolPositions(owner, repo, ref, symbol); + + return LSIFReferencesPromise.then((LSIFReferences) => { + if (LSIFReferences.length) { + return LSIFReferences; + } + return searchReferencesPromise as Promise; + }); +}; diff --git a/extensions/github1s/src/interfaces/sourcegraph-api.ts b/extensions/github1s/src/interfaces/sourcegraph/search.ts similarity index 55% rename from extensions/github1s/src/interfaces/sourcegraph-api.ts rename to extensions/github1s/src/interfaces/sourcegraph/search.ts index bd8c742dc..e0af80582 100644 --- a/extensions/github1s/src/interfaces/sourcegraph-api.ts +++ b/extensions/github1s/src/interfaces/sourcegraph/search.ts @@ -1,28 +1,54 @@ /** - * @file source graph api + * @file Sourcegraph search api * @author netcon */ -import { - ApolloClient, - createHttpLink, - InMemoryCache, - gql, -} from '@apollo/client/core'; +import { gql } from '@apollo/client/core'; import { TextSearchQuery, TextSearchOptions } from 'vscode'; -import { trimStart, trimEnd } from '@/helpers/util'; +import { + escapeRegexp, + sourcegraphClient, + combineGlobsToRegExp, + getRepoRefQueryString, +} from './common'; + +export const buildTextSearchQueryString = ( + owner: string, + repo: string, + ref: string, + query: TextSearchQuery, + options: TextSearchOptions +): string => { + const repoRefQueryString = getRepoRefQueryString(owner, repo, ref); + // the string may looks like `case:yse file:src -file:node_modules` + const optionsString = [ + query.isCaseSensitive ? `case:yes` : '', + options.includes?.length + ? `file:${combineGlobsToRegExp(options.includes)}` + : '', + options.excludes?.length + ? `-file:${combineGlobsToRegExp(options.excludes)}` + : '', + ] + .filter(Boolean) + .join(' '); + // convert the pattern to adapt the sourcegraph API + let patternString = query.pattern; -const sourcegraphLink = createHttpLink({ - // Since the Sourcegraph refused the CORS check now, - // use Vercel Serverless Function to proxy it temporarily - // See `/api/sourcegraph.js` - uri: '/api/sourcegraph', -}); + if (!query.isRegExp && !query.isWordMatch) { + patternString = `"${patternString}"`; + } else if (!query.isRegExp && query.isWordMatch) { + patternString = `/\\b${escapeRegexp(patternString)}\\b/`; + } else if (query.isRegExp && !query.isWordMatch) { + patternString = `/${patternString}/`; + } else if (query.isRegExp && query.isWordMatch) { + return `/\b${patternString}\b/`; + } -const sourcegraphClient = new ApolloClient({ - link: sourcegraphLink, - cache: new InMemoryCache(), -}); + return [repoRefQueryString, optionsString, patternString] + .filter(Boolean) + .join(' '); +}; const textSearchQuery = gql` query($query: String!) { @@ -61,75 +87,6 @@ const textSearchQuery = gql` } `; -const canBeConvertToRegExp = (str: string) => { - try { - new RegExp(str); - return true; - } catch (e) { - return false; - } -}; - -const combineGlobsToRegExp = (globs: string[]) => { - // only support very simple globs convert now - const result = Array.from( - new Set( - globs.map((glob: string) => - trimEnd(trimStart(glob, '*/'), '*/').replace(/^\./, '\\.') - ) - ) - ) - // if the glob still not can be convert to a regexp, just ignore it - .filter((item) => canBeConvertToRegExp(item)) - .join('|'); - // ensure the result can be convert to a regexp - return canBeConvertToRegExp(result) ? result : ''; -}; - -const escapeRegexp = (text: string): string => - text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); - -const buildTextSearchQueryString = ( - owner: string, - repo: string, - ref: string, - query: TextSearchQuery, - options: TextSearchOptions -): string => { - // the string may looks like `repo:^github\.com/conwnet/github1s` - const repoPattern = escapeRegexp(`github\.com/${owner}/${repo}`); - const repoStringWithRef = - ref === 'HEAD' ? `repo:^${repoPattern}$` : `repo:^${repoPattern}$@${ref}`; - // the string may looks like `case:yse file:src -file:node_modules` - const optionsString = [ - query.isCaseSensitive ? `case:yes` : '', - options.includes?.length - ? `file:${combineGlobsToRegExp(options.includes)}` - : '', - options.excludes?.length - ? `-file:${combineGlobsToRegExp(options.excludes)}` - : '', - ] - .filter(Boolean) - .join(' '); - // convert the pattern to adapt the sourcegraph API - let patternString = query.pattern; - - if (!query.isRegExp && !query.isWordMatch) { - patternString = `"${patternString}"`; - } else if (!query.isRegExp && query.isWordMatch) { - patternString = `/\\b${escapeRegexp(patternString)}\\b/`; - } else if (query.isRegExp && !query.isWordMatch) { - patternString = `/${patternString}/`; - } else if (query.isRegExp && query.isWordMatch) { - return `/\b${patternString}\b/`; - } - - return [repoStringWithRef, optionsString, patternString] - .filter(Boolean) - .join(' '); -}; - export const getTextSearchResults = ( owner: string, repo: string, diff --git a/extensions/github1s/src/listeners/vscode.ts b/extensions/github1s/src/listeners/vscode.ts index 1e52f48a3..a38841e57 100644 --- a/extensions/github1s/src/listeners/vscode.ts +++ b/extensions/github1s/src/listeners/vscode.ts @@ -9,6 +9,7 @@ import { PageType } from '@/router/types'; import { setVSCodeContext } from '@/helpers/vscode'; import { getChangedFileFromSourceControl } from '@/commands/editor'; import { GitHub1sFileSystemProvider } from '@/providers/fileSystemProvider'; +import { debounce } from '@/helpers/func'; const handleRouterOnActiveEditorChange = async ( editor: vscode.TextEditor | undefined @@ -33,12 +34,12 @@ const handleRouterOnActiveEditorChange = async ( ref.toUpperCase() === 'HEAD' ? `/${owner}/${repo}` : `/${owner}/${repo}/tree/${ref}`; - router.history.replace(browserPath); + router.history.replace({ pathname: browserPath, hash: '' }); return; } const browserPath = `/${owner}/${repo}/blob/${ref}${activeFileUri.path}`; - router.history.replace(browserPath); + router.history.replace({ pathname: browserPath, hash: '' }); }; // if the `Open Changes` Button should show in editor title @@ -60,10 +61,51 @@ const handleGutterBlameOpeningContextOnActiveEditorChange = async () => { return setVSCodeContext('github1s.context.gutterBlameOpening', false); }; +// get the line anchor hash (`#L27-L39`) from the editor.selection +const getAnchorHashFromSelection = (selection: vscode.Selection) => { + const { start, end } = selection; + + // the cursor move to somewhere but nothing is selected + if (start.line === end.line && start.character === end.character) { + return ''; + } + if (start.line === end.line) { + return `#L${start.line + 1}`; + } + return `#L${start.line + 1}-L${end.line + 1}`; +}; + +// add the line number anchor when user selection lines in a editor +const handleRouterOnTextEditorSelectionChange = async ( + editor: vscode.TextEditor +) => { + const { owner, repo, ref, pageType } = await router.getState(); + + // only add the line number anchor when pageType is PageType.BLOB + if (pageType !== PageType.BLOB || !editor.selection) { + return; + } + + const activeFileUri = editor?.document.uri; + const anchorText = getAnchorHashFromSelection(editor.selection); + const browserPath = `/${owner}/${repo}/blob/${ref}${activeFileUri.path}`; + + router.history.replace({ pathname: browserPath, hash: anchorText }); +}; + export const registerVSCodeEventListeners = () => { - vscode.window.onDidChangeActiveTextEditor(async (editor) => { + vscode.window.onDidChangeActiveTextEditor((editor) => { handleRouterOnActiveEditorChange(editor); handleOpenChangesContextOnActiveEditorChange(editor); handleGutterBlameOpeningContextOnActiveEditorChange(); }); + + // debounce to update the browser url + const debouncedSelectionChangeRouterHandler = debounce( + handleRouterOnTextEditorSelectionChange, + 100 + ); + vscode.window.onDidChangeTextEditorSelection((event) => { + debouncedSelectionChangeRouterHandler(event.textEditor); + }); }; diff --git a/extensions/github1s/src/messages.ts b/extensions/github1s/src/messages.ts index 7798c1513..eddb558b6 100644 --- a/extensions/github1s/src/messages.ts +++ b/extensions/github1s/src/messages.ts @@ -3,17 +3,42 @@ */ import * as vscode from 'vscode'; +import router from '@/router'; +import { getSourcegraphUrl } from '@/helpers/urls'; export const showSourcegraphSearchMessage = (() => { let alreadyShown = false; - return () => { + return async () => { if (alreadyShown) { return; } alreadyShown = true; + const { owner, repo, ref } = await router.getState(); + const url = `https://sourcegraph.com/github.com/${owner}/${repo}@${ref}`; vscode.window.showInformationMessage( - 'The code search ability is powered by [Sourcegraph](https://sourcegraph.com)' + `The code search ability is powered by [Sourcegraph](${url})` + ); + }; +})(); + +export const showSourcegraphSymbolMessage = (() => { + let alreadyShown = false; + return async ( + owner: string, + repo: string, + ref: string, + path: string, + line: number, + character: number + ) => { + if (alreadyShown) { + return; + } + alreadyShown = true; + const url = getSourcegraphUrl(owner, repo, ref, path, line, character); + vscode.window.showInformationMessage( + `The results is provided by [Sourcegraph](${url})` ); }; })(); diff --git a/extensions/github1s/src/providers/definitionProvider.ts b/extensions/github1s/src/providers/definitionProvider.ts new file mode 100644 index 000000000..aee0c12d5 --- /dev/null +++ b/extensions/github1s/src/providers/definitionProvider.ts @@ -0,0 +1,69 @@ +/** + * @file DefinitionProvider + * @author netcon + */ + +import * as vscode from 'vscode'; +import router from '@/router'; +import { getSymbolDefinitions } from '@/interfaces/sourcegraph/definition'; +import { showSourcegraphSymbolMessage } from '@/messages'; +import { GitHub1sFileSystemProvider } from './fileSystemProvider'; + +export class GitHub1sDefinitionProvider implements vscode.DefinitionProvider { + static scheme = 'github1s'; + + async provideDefinition( + document: vscode.TextDocument, + position: vscode.Position, + _token: vscode.CancellationToken + ): Promise { + const symbolRange = document.getWordRangeAtPosition(position); + const symbol = symbolRange ? document.getText(symbolRange) : ''; + + if (!symbol) { + return; + } + + const authority = document.uri.authority || (await router.getAuthority()); + const [owner, repo, ref] = authority.split('+').filter(Boolean); + const path = document.uri.path; + const { line, character } = position; + + const symbolDefinitions = await getSymbolDefinitions( + owner, + repo, + ref, + path, + line, + character, + symbol + ); + + if (symbolDefinitions.length) { + showSourcegraphSymbolMessage(owner, repo, ref, path, line, character); + } + + return symbolDefinitions.map((repoDefinition) => { + const isSameRepo = + repoDefinition.owner === owner && repoDefinition.repo === repo; + // if the definition target and the searched symbol is in the same + // repository, just replace the `document.uri.path` with targetPath + // (so that the target file will open with expanding the file explorer) + const uri = isSameRepo + ? document.uri.with({ path: repoDefinition.path }) + : vscode.Uri.parse('').with({ + scheme: GitHub1sFileSystemProvider.scheme, + authority: `${owner}+${repo}+${ref}`, + path: repoDefinition.path, + }); + const { start, end } = repoDefinition.range; + return { + uri, + range: new vscode.Range( + new vscode.Position(start.line, start.character), + new vscode.Position(end.line, end.character) + ), + }; + }); + } +} diff --git a/extensions/github1s/src/providers/hoverProvider.ts b/extensions/github1s/src/providers/hoverProvider.ts new file mode 100644 index 000000000..63d25c4b5 --- /dev/null +++ b/extensions/github1s/src/providers/hoverProvider.ts @@ -0,0 +1,117 @@ +/** + * @file HoverProvider + * @author netcon + */ + +import * as vscode from 'vscode'; +import router from '@/router'; +import { getSymbolHover, SymbolHover } from '@/interfaces/sourcegraph/hover'; +import { getSymbolPositions } from '@/interfaces/sourcegraph/position'; +import { getSourcegraphUrl } from '@/helpers/urls'; + +const getSemanticMarkdownSuffix = (sourcegraphUrl: String) => ` + +--- + +[Semantic](https://docs.sourcegraph.com/code_intelligence/explanations/precise_code_intelligence) result provided by [Sourcegraph](${sourcegraphUrl}) +`; + +const getSearchBasedMarkdownSuffix = (sourcegraphUrl: String) => ` + +--- + +[Search-based](https://docs.sourcegraph.com/code_intelligence/explanations/precise_code_intelligence) result provided by [Sourcegraph](${sourcegraphUrl}) +`; + +export class GitHub1sHoverProvider implements vscode.HoverProvider { + static scheme = 'github1s'; + + async getSearchBasedHover( + document: vscode.TextDocument, + symbol: string + ): Promise { + const authority = document.uri.authority || (await router.getAuthority()); + const [owner, repo, ref] = authority.split('+').filter(Boolean); + const definitions = await getSymbolPositions(owner, repo, ref, symbol); + + if (!definitions.length) { + return null; + } + + // use the information of first definition as hover context + const target = definitions[0]; + const isSameRepo = target.owner === owner && target.repo === repo; + // if the definition target and the searched symbol is in the same + // repository, just replace the `document.uri.path` with targetPath + const targetFileUri = isSameRepo + ? document.uri.with({ path: target.path }) + : vscode.Uri.parse('').with({ + scheme: GitHub1sHoverProvider.scheme, + authority: `${target.owner}+${target.repo}+${target.ref}`, + path: target.path, + }); + // open corresponding file with target + const textDocument = await vscode.workspace.openTextDocument(targetFileUri); + // get the content in `[range.start.line - 2, range.end.line + 2]` lines + const startPosition = new vscode.Position( + Math.max(0, target.range.start.line - 2), + 0 + ); + const endPosition = textDocument.lineAt( + Math.min(textDocument.lineCount - 1, target.range.end.line + 2) + ).range.end; + const codeText = textDocument.getText( + new vscode.Range(startPosition, endPosition) + ); + + return { + precise: false, + markdown: `\`\`\`${textDocument.languageId}\n${codeText}\n\`\`\``, + }; + } + + async provideHover( + document: vscode.TextDocument, + position: vscode.Position, + _token: vscode.CancellationToken + ): Promise { + const symbolRange = document.getWordRangeAtPosition(position); + const symbol = symbolRange ? document.getText(symbolRange) : ''; + + if (!symbol) { + return; + } + + const authority = document.uri.authority || (await router.getAuthority()); + const [owner, repo, ref] = authority.split('+').filter(Boolean); + const path = document.uri.path; + const { line, character } = position; + + type ParamsType = [string, string, string, string, number, number]; + const requestParams: ParamsType = [owner, repo, ref, path, line, character]; + // get the sourcegraph url for current symbol + const sourcegraphUrl = getSourcegraphUrl(...requestParams); + // get the hover result based on sourcegraph lsif + const preciseHoverPromise = getSymbolHover(...requestParams); + // get the hover result based on search + const searchBasedHoverPromise = this.getSearchBasedHover(document, symbol); + + const symbolHover = await preciseHoverPromise.then((symbolHover) => { + if (symbolHover) { + return symbolHover; + } + // fallback to search based result if we can not get precise result + return searchBasedHoverPromise; + }); + + if (!symbolHover) { + return null; + } + + const suffixMarkdown = symbolHover.precise + ? getSemanticMarkdownSuffix(sourcegraphUrl) + : getSearchBasedMarkdownSuffix(sourcegraphUrl); + + return new vscode.Hover(symbolHover.markdown + suffixMarkdown); + } +} diff --git a/extensions/github1s/src/providers/index.ts b/extensions/github1s/src/providers/index.ts index 38f265062..99b954028 100644 --- a/extensions/github1s/src/providers/index.ts +++ b/extensions/github1s/src/providers/index.ts @@ -11,6 +11,9 @@ import { GitHub1sTextSearchProvider } from './textSearchProvider'; import { GitHub1sSubmoduleDecorationProvider } from './submoduleDecorationProvider'; import { GitHub1sChangedFileDecorationProvider } from './changedFileDecorationProvider'; import { GitHub1sSourceControlDecorationProvider } from './sourceControlDecorationProvider'; +import { GitHub1sDefinitionProvider } from './definitionProvider'; +import { GitHub1sReferenceProvider } from './referenceProvider'; +import { GitHub1sHoverProvider } from './hoverProvider'; export const fileSystemProvider = new GitHub1sFileSystemProvider(); export const fileSearchProvider = new GitHub1sFileSearchProvider( @@ -22,6 +25,9 @@ export const submoduleDecorationProvider = new GitHub1sSubmoduleDecorationProvid ); export const changedFileDecorationProvider = new GitHub1sChangedFileDecorationProvider(); export const sourceControlDecorationProvider = new GitHub1sSourceControlDecorationProvider(); +export const definitionProvider = new GitHub1sDefinitionProvider(); +export const referenceProvider = new GitHub1sReferenceProvider(); +export const hoverProvider = new GitHub1sHoverProvider(); export const EMPTY_FILE_SCHEME = 'github1s-empty-file'; export const emptyFileUri = vscode.Uri.parse('').with({ @@ -35,10 +41,7 @@ export const registerVSCodeProviders = () => { vscode.workspace.registerFileSystemProvider( GitHub1sFileSystemProvider.scheme, fileSystemProvider, - { - isCaseSensitive: true, - isReadonly: true, - } + { isCaseSensitive: true, isReadonly: true } ), vscode.workspace.registerFileSearchProvider( GitHub1sFileSearchProvider.scheme, @@ -54,6 +57,19 @@ export const registerVSCodeProviders = () => { sourceControlDecorationProvider ), + vscode.languages.registerDefinitionProvider( + { scheme: GitHub1sDefinitionProvider.scheme }, + definitionProvider + ), + vscode.languages.registerReferenceProvider( + { scheme: GitHub1sReferenceProvider.scheme }, + referenceProvider + ), + vscode.languages.registerHoverProvider( + { scheme: GitHub1sHoverProvider.scheme }, + hoverProvider + ), + // provider a readonly empty file for diff vscode.workspace.registerTextDocumentContentProvider(EMPTY_FILE_SCHEME, { provideTextDocumentContent: () => '', diff --git a/extensions/github1s/src/providers/referenceProvider.ts b/extensions/github1s/src/providers/referenceProvider.ts new file mode 100644 index 000000000..f98e4c4e8 --- /dev/null +++ b/extensions/github1s/src/providers/referenceProvider.ts @@ -0,0 +1,70 @@ +/** + * @file ReferenceProvider + * @author netcon + */ + +import * as vscode from 'vscode'; +import router from '@/router'; +import { getSymbolReferences } from '@/interfaces/sourcegraph/reference'; +import { showSourcegraphSymbolMessage } from '@/messages'; +import { GitHub1sFileSystemProvider } from './fileSystemProvider'; + +export class GitHub1sReferenceProvider implements vscode.ReferenceProvider { + static scheme = 'github1s'; + + async provideReferences( + document: vscode.TextDocument, + position: vscode.Position, + _context: vscode.ReferenceContext, + _token: vscode.CancellationToken + ): Promise { + const symbolRange = document.getWordRangeAtPosition(position); + const symbol = symbolRange ? document.getText(symbolRange) : ''; + + if (!symbol) { + return; + } + + const authority = document.uri.authority || (await router.getAuthority()); + const [owner, repo, ref] = authority.split('+').filter(Boolean); + const path = document.uri.path; + const { line, character } = position; + + const symbolReferences = await getSymbolReferences( + owner, + repo, + ref, + path, + line, + character, + symbol + ); + + if (symbolReferences.length) { + showSourcegraphSymbolMessage(owner, repo, ref, path, line, character); + } + + return symbolReferences.map((repoReference) => { + const isSameRepo = + repoReference.owner === owner && repoReference.repo === repo; + // if the reference target and the searched symbol is in the same + // repository, just replace the `document.uri.path` with targetPath + // (so that the target file will open with expanding the file explorer) + const uri = isSameRepo + ? document.uri.with({ path: repoReference.path }) + : vscode.Uri.parse('').with({ + scheme: GitHub1sFileSystemProvider.scheme, + authority: `${owner}+${repo}+${ref}`, + path: repoReference.path, + }); + const { start, end } = repoReference.range; + return { + uri, + range: new vscode.Range( + new vscode.Position(start.line, start.character), + new vscode.Position(end.line, end.character) + ), + }; + }); + } +} diff --git a/extensions/github1s/src/providers/textSearchProvider.ts b/extensions/github1s/src/providers/textSearchProvider.ts index 0ca30769a..bf40598ee 100644 --- a/extensions/github1s/src/providers/textSearchProvider.ts +++ b/extensions/github1s/src/providers/textSearchProvider.ts @@ -17,7 +17,7 @@ import { Uri, } from 'vscode'; import router from '@/router'; -import { getTextSearchResults } from '@/interfaces/sourcegraph-api'; +import { getTextSearchResults } from '@/interfaces/sourcegraph/search'; import { showSourcegraphSearchMessage } from '@/messages'; import { GitHub1sFileSystemProvider } from './fileSystemProvider'; diff --git a/extensions/github1s/src/router/parser/blob.ts b/extensions/github1s/src/router/parser/blob.ts index 7b589d300..28d150e05 100644 --- a/extensions/github1s/src/router/parser/blob.ts +++ b/extensions/github1s/src/router/parser/blob.ts @@ -3,11 +3,27 @@ * @author netcon */ +import { parsePath } from 'history'; import { PageType, RouterState } from '../types'; import { parseTreeUrl } from './tree'; export const parseBlobUrl = async (path: string): Promise => { const routerState = await parseTreeUrl(path); + const { hash: routerHash } = parsePath(path); - return { ...routerState, pageType: PageType.BLOB }; + if (!routerHash) { + return { ...routerState, pageType: PageType.BLOB }; + } + + // get selected line number range from path which looks like: + // `/conwnet/github1s/blob/HEAD/package.json#L10-L20` + const matches = routerHash.match(/^#L(\d+)(?:-L(\d+))?/); + const [_, startLineNumber = '0', endLineNumber] = matches ? matches : []; + + return { + ...routerState, + pageType: PageType.BLOB, + startLineNumber: parseInt(startLineNumber, 10), + endLineNumber: parseInt(endLineNumber || startLineNumber, 10), + }; }; diff --git a/extensions/github1s/src/router/types.ts b/extensions/github1s/src/router/types.ts index 3e1fb557b..1a93ad127 100644 --- a/extensions/github1s/src/router/types.ts +++ b/extensions/github1s/src/router/types.ts @@ -52,6 +52,11 @@ export interface RouterState { // current file path filePath?: string; + // the one-based line numbers specified which lines should focus + // after opening the file, only exists when pageType is `BLOB` + startLineNumber?: number; + endLineNumber?: number; + // only exists when the page type is PULL pullNumber?: number; diff --git a/extensions/github1s/src/sponsors.ts b/extensions/github1s/src/sponsors.ts index ae2c2c8e0..5df5c99b9 100644 --- a/extensions/github1s/src/sponsors.ts +++ b/extensions/github1s/src/sponsors.ts @@ -4,21 +4,26 @@ */ import * as vscode from 'vscode'; +import router from '@/router'; -const sponsors = [ - { - name: 'Vercel', - link: 'https://vercel.com/?utm_source=vscode-github1s&utm_campaign=oss', - description: 'Develop. Preview. Ship.', - }, - { - name: 'Sourcegraph', - link: 'https://sourcegraph.com', - description: 'Universal code search', - }, -]; +const resolveSponsors = async () => { + const { owner, repo, ref } = await router.getState(); -export const showSponsors = () => { + return [ + { + name: 'Vercel', + link: 'https://vercel.com/?utm_source=vscode-github1s&utm_campaign=oss', + description: 'Develop. Preview. Ship.', + }, + { + name: 'Sourcegraph', + link: `https://sourcegraph.com/github.com/${owner}/${repo}@${ref}`, + description: 'Universal code search', + }, + ]; +}; + +export const showSponsors = async () => { const titleItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Left, 0 @@ -26,7 +31,7 @@ export const showSponsors = () => { titleItem.text = ' $(heart) Sponsors:'; titleItem.show(); - sponsors.forEach((sponsor) => { + (await resolveSponsors()).forEach((sponsor) => { const sponsorItem = vscode.window.createStatusBarItem( vscode.StatusBarAlignment.Left, 0 diff --git a/extensions/github1s/yarn.lock b/extensions/github1s/yarn.lock index 879c09749..205353d9c 100644 --- a/extensions/github1s/yarn.lock +++ b/extensions/github1s/yarn.lock @@ -308,25 +308,25 @@ braces@^3.0.1: fill-range "^7.0.1" browserslist@^4.14.5: - version "4.16.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" - integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== + version "4.16.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" + integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== dependencies: - caniuse-lite "^1.0.30001181" - colorette "^1.2.1" - electron-to-chromium "^1.3.649" + caniuse-lite "^1.0.30001219" + colorette "^1.2.2" + electron-to-chromium "^1.3.723" escalade "^3.1.1" - node-releases "^1.1.70" + node-releases "^1.1.71" buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -caniuse-lite@^1.0.30001181: - version "1.0.30001207" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001207.tgz#364d47d35a3007e528f69adb6fecb07c2bb2cc50" - integrity sha512-UPQZdmAsyp2qfCTiMU/zqGSWOYaY9F9LL61V8f+8MrubsaDGpaHD9HRV/EWZGULZn0Hxu48SKzI5DgFwTvHuYw== +caniuse-lite@^1.0.30001219: + version "1.0.30001230" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz#8135c57459854b2240b57a4a6786044bdc5a9f71" + integrity sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ== chalk@^4.1.0: version "4.1.0" @@ -364,10 +364,10 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colorette@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== +colorette@^1.2.1, colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== commander@^2.20.0: version "2.20.3" @@ -403,10 +403,10 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= -electron-to-chromium@^1.3.649: - version "1.3.709" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.709.tgz#d7be0b5686a2fdfe8bad898faa3a428d04d8f656" - integrity sha512-LolItk2/ikSGQ7SN8UkuKVNMBZp3RG7Itgaxj1npsHRzQobj9JjMneZOZfLhtwlYBe5fCJ75k+cVCiDFUs23oA== +electron-to-chromium@^1.3.723: + version "1.3.740" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.740.tgz#e38b7d2b848f632191b643e6dabca51be2162922" + integrity sha512-Mi2m55JrX2BFbNZGKYR+2ItcGnR4O5HhrvgoRRyZQlaMGQULqDhoGkLWHzJoshSzi7k1PUofxcDbNhlFrDZNhg== emojis-list@^3.0.0: version "3.0.0" @@ -808,10 +808,10 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -node-releases@^1.1.70: - version "1.1.71" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" - integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== +node-releases@^1.1.71: + version "1.1.72" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe" + integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw== npm-run-path@^4.0.1: version "4.0.1" diff --git a/extensions/jupyter-web/yarn.lock b/extensions/jupyter-web/yarn.lock index 5dcc9f27c..85554b040 100644 --- a/extensions/jupyter-web/yarn.lock +++ b/extensions/jupyter-web/yarn.lock @@ -305,15 +305,15 @@ brace-expansion@^1.1.7: concat-map "0.0.1" browserslist@^4.14.5: - version "4.16.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" - integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== + version "4.16.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" + integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== dependencies: - caniuse-lite "^1.0.30001181" - colorette "^1.2.1" - electron-to-chromium "^1.3.649" + caniuse-lite "^1.0.30001219" + colorette "^1.2.2" + electron-to-chromium "^1.3.723" escalade "^3.1.1" - node-releases "^1.1.70" + node-releases "^1.1.71" buffer-from@^1.0.0: version "1.1.1" @@ -332,10 +332,10 @@ callsites@^0.2.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo= -caniuse-lite@^1.0.30001181: - version "1.0.30001187" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001187.tgz#5706942631f83baa5a0218b7dfa6ced29f845438" - integrity sha512-w7/EP1JRZ9552CyrThUnay2RkZ1DXxKe/Q2swTC4+LElLh9RRYrL1Z+27LlakB8kzY0fSmHw9mc7XYDUKAKWMA== +caniuse-lite@^1.0.30001219: + version "1.0.30001230" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz#8135c57459854b2240b57a4a6786044bdc5a9f71" + integrity sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ== chalk@^1.1.3: version "1.1.3" @@ -412,10 +412,10 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -colorette@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== +colorette@^1.2.1, colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== commander@^2.20.0: version "2.20.3" @@ -484,10 +484,10 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" -electron-to-chromium@^1.3.649: - version "1.3.665" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.665.tgz#6d0937376f6a919c0f289202c4be77790a6175e5" - integrity sha512-LIjx1JheOz7LM8DMEQ2tPnbBzJ4nVG1MKutsbEMLnJfwfVdPIsyagqfLp56bOWhdBrYGXWHaTayYkllIU2TauA== +electron-to-chromium@^1.3.723: + version "1.3.739" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.739.tgz#f07756aa92cabd5a6eec6f491525a64fe62f98b9" + integrity sha512-+LPJVRsN7hGZ9EIUUiWCpO7l4E3qBYHNadazlucBfsXBbccDFNKUBAgzE68FnkWGJPwD/AfKhSzL+G+Iqb8A4A== enhanced-resolve@^5.7.0: version "5.7.0" @@ -1047,10 +1047,10 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -node-releases@^1.1.70: - version "1.1.70" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.70.tgz#66e0ed0273aa65666d7fe78febe7634875426a08" - integrity sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw== +node-releases@^1.1.71: + version "1.1.72" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe" + integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw== npm-run-path@^4.0.1: version "4.0.1" diff --git a/package.json b/package.json index 87c9ccebf..e7d538cc2 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "lib": "lib" }, "devDependencies": { - "@github1s/vscode-web": "0.1.8", + "@github1s/vscode-web": "0.1.10", "@typescript-eslint/eslint-plugin": "^4.15.0", "@typescript-eslint/parser": "^4.15.0", "chokidar": "^3.5.1", diff --git a/resources/images/sourcegraph-logo.svg b/resources/images/sourcegraph-logo.svg index 3367c8aca..6bc102b3a 100644 --- a/resources/images/sourcegraph-logo.svg +++ b/resources/images/sourcegraph-logo.svg @@ -1,66 +1,79 @@ - + + viewBox="0 0 350 100" style="enable-background:new 0 0 350 100;" xml:space="preserve"> - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/robots.txt b/resources/robots.txt new file mode 100644 index 000000000..c2a49f4fb --- /dev/null +++ b/resources/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/scripts/package/copy-resources.sh b/scripts/package/copy-resources.sh index 7d78bcf4f..9eeaa0bb7 100755 --- a/scripts/package/copy-resources.sh +++ b/scripts/package/copy-resources.sh @@ -15,6 +15,7 @@ function main() { fi cp resources/favicon* dist cp resources/manifest.json dist + cp resources/robots.txt dist echo "copy resources done!" } diff --git a/tests/yarn.lock b/tests/yarn.lock index f4f3c1c6e..4f5e6a22c 100644 --- a/tests/yarn.lock +++ b/tests/yarn.lock @@ -967,15 +967,15 @@ browser-process-hrtime@^1.0.0: integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== browserslist@^4.14.5: - version "4.16.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" - integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== + version "4.16.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" + integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== dependencies: - caniuse-lite "^1.0.30001181" - colorette "^1.2.1" - electron-to-chromium "^1.3.649" + caniuse-lite "^1.0.30001219" + colorette "^1.2.2" + electron-to-chromium "^1.3.723" escalade "^3.1.1" - node-releases "^1.1.70" + node-releases "^1.1.71" bs-logger@0.x: version "0.2.6" @@ -1041,10 +1041,10 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== -caniuse-lite@^1.0.30001181: - version "1.0.30001191" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz#bacb432b6701f690c8c5f7c680166b9a9f0843d9" - integrity sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw== +caniuse-lite@^1.0.30001219: + version "1.0.30001230" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz#8135c57459854b2240b57a4a6786044bdc5a9f71" + integrity sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ== capture-exit@^2.0.0: version "2.0.0" @@ -1172,10 +1172,10 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colorette@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== +colorette@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" @@ -1400,10 +1400,10 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -electron-to-chromium@^1.3.649: - version "1.3.673" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.673.tgz#b4f81c930b388f962b7eba20d0483299aaa40913" - integrity sha512-ms+QR2ckfrrpEAjXweLx6kNCbpAl66DcW//3BZD4BV5KhUgr0RZRce1ON/9J3QyA3JO28nzgb5Xv8DnPr05ILg== +electron-to-chromium@^1.3.723: + version "1.3.740" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.740.tgz#e38b7d2b848f632191b643e6dabca51be2162922" + integrity sha512-Mi2m55JrX2BFbNZGKYR+2ItcGnR4O5HhrvgoRRyZQlaMGQULqDhoGkLWHzJoshSzi7k1PUofxcDbNhlFrDZNhg== emittery@^0.7.1: version "0.7.2" @@ -3121,10 +3121,10 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" -node-releases@^1.1.70: - version "1.1.71" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" - integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== +node-releases@^1.1.71: + version "1.1.72" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe" + integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw== normalize-package-data@^2.5.0: version "2.5.0" @@ -4477,9 +4477,9 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^7.2.3, ws@^7.3.1: - version "7.4.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" - integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== xml-name-validator@^3.0.0: version "3.0.0" diff --git a/vscode-web-github1s/extensions/typescript-language-features/src/typescriptServiceClient.ts b/vscode-web-github1s/extensions/typescript-language-features/src/typescriptServiceClient.ts new file mode 100644 index 000000000..c28ec016f --- /dev/null +++ b/vscode-web-github1s/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -0,0 +1,1050 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { DiagnosticKind, DiagnosticsManager } from './languageFeatures/diagnostics'; +import * as Proto from './protocol'; +import { EventName } from './protocol.const'; +import BufferSyncSupport from './tsServer/bufferSyncSupport'; +import { OngoingRequestCancellerFactory } from './tsServer/cancellation'; +import { ILogDirectoryProvider } from './tsServer/logDirectoryProvider'; +import { ITypeScriptServer, TsServerProcessFactory } from './tsServer/server'; +import { TypeScriptServerError } from './tsServer/serverError'; +import { TypeScriptServerSpawner } from './tsServer/spawner'; +import { TypeScriptVersionManager } from './tsServer/versionManager'; +import { ITypeScriptVersionProvider, TypeScriptVersion } from './tsServer/versionProvider'; +import { ClientCapabilities, ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse, TypeScriptRequests } from './typescriptService'; +import API from './utils/api'; +import { TsServerLogLevel, TypeScriptServiceConfiguration } from './utils/configuration'; +import { Disposable } from './utils/dispose'; +import * as fileSchemes from './utils/fileSchemes'; +import { Logger } from './utils/logger'; +import { isWeb } from './utils/platform'; +import { TypeScriptPluginPathsProvider } from './utils/pluginPathsProvider'; +import { PluginManager } from './utils/plugins'; +import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './utils/telemetry'; +import Tracer from './utils/tracer'; +import { inferredProjectCompilerOptions, ProjectType } from './utils/tsconfig'; + +const localize = nls.loadMessageBundle(); + +export interface TsDiagnostics { + readonly kind: DiagnosticKind; + readonly resource: vscode.Uri; + readonly diagnostics: Proto.Diagnostic[]; +} + +interface ToCancelOnResourceChanged { + readonly resource: vscode.Uri; + cancel(): void; +} + +namespace ServerState { + export const enum Type { + None, + Running, + Errored + } + + export const None = { type: Type.None } as const; + + export class Running { + readonly type = Type.Running; + + constructor( + public readonly server: ITypeScriptServer, + + /** + * API version obtained from the version picker after checking the corresponding path exists. + */ + public readonly apiVersion: API, + + /** + * Version reported by currently-running tsserver. + */ + public tsserverVersion: string | undefined, + public languageServiceEnabled: boolean, + ) { } + + public readonly toCancelOnResourceChange = new Set(); + + updateTsserverVersion(tsserverVersion: string) { + this.tsserverVersion = tsserverVersion; + } + + updateLanguageServiceEnabled(enabled: boolean) { + this.languageServiceEnabled = enabled; + } + } + + export class Errored { + readonly type = Type.Errored; + constructor( + public readonly error: Error, + public readonly tsServerLogFile: string | undefined, + ) { } + } + + export type State = typeof None | Running | Errored; +} + +export default class TypeScriptServiceClient extends Disposable implements ITypeScriptServiceClient { + + private readonly pathSeparator: string; + private readonly inMemoryResourcePrefix = '^'; + + private readonly workspaceState: vscode.Memento; + + private _onReady?: { promise: Promise; resolve: () => void; reject: () => void; }; + private _configuration: TypeScriptServiceConfiguration; + private pluginPathsProvider: TypeScriptPluginPathsProvider; + private readonly _versionManager: TypeScriptVersionManager; + + private readonly logger = new Logger(); + private readonly tracer = new Tracer(this.logger); + + private readonly typescriptServerSpawner: TypeScriptServerSpawner; + private serverState: ServerState.State = ServerState.None; + private lastStart: number; + private numberRestarts: number; + private _isPromptingAfterCrash = false; + private isRestarting: boolean = false; + private hasServerFatallyCrashedTooManyTimes = false; + private readonly loadingIndicator = new ServerInitializingIndicator(); + + public readonly telemetryReporter: TelemetryReporter; + public readonly bufferSyncSupport: BufferSyncSupport; + public readonly diagnosticsManager: DiagnosticsManager; + public readonly pluginManager: PluginManager; + + private readonly logDirectoryProvider: ILogDirectoryProvider; + private readonly cancellerFactory: OngoingRequestCancellerFactory; + private readonly versionProvider: ITypeScriptVersionProvider; + private readonly processFactory: TsServerProcessFactory; + + constructor( + private readonly context: vscode.ExtensionContext, + onCaseInsenitiveFileSystem: boolean, + services: { + pluginManager: PluginManager, + logDirectoryProvider: ILogDirectoryProvider, + cancellerFactory: OngoingRequestCancellerFactory, + versionProvider: ITypeScriptVersionProvider, + processFactory: TsServerProcessFactory, + }, + allModeIds: readonly string[] + ) { + super(); + + this.workspaceState = context.workspaceState; + + this.pluginManager = services.pluginManager; + this.logDirectoryProvider = services.logDirectoryProvider; + this.cancellerFactory = services.cancellerFactory; + this.versionProvider = services.versionProvider; + this.processFactory = services.processFactory; + + this.pathSeparator = path.sep; + this.lastStart = Date.now(); + + let resolve: () => void; + let reject: () => void; + const p = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + this._onReady = { promise: p, resolve: resolve!, reject: reject! }; + + this.numberRestarts = 0; + + this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace(); + this.versionProvider.updateConfiguration(this._configuration); + + this.pluginPathsProvider = new TypeScriptPluginPathsProvider(this._configuration); + this._versionManager = this._register(new TypeScriptVersionManager(this._configuration, this.versionProvider, this.workspaceState)); + this._register(this._versionManager.onDidPickNewVersion(() => { + this.restartTsServer(); + })); + + this.bufferSyncSupport = new BufferSyncSupport(this, allModeIds, onCaseInsenitiveFileSystem); + this.onReady(() => { this.bufferSyncSupport.listen(); }); + + this.diagnosticsManager = new DiagnosticsManager('typescript', onCaseInsenitiveFileSystem); + this.bufferSyncSupport.onDelete(resource => { + this.cancelInflightRequestsForResource(resource); + this.diagnosticsManager.delete(resource); + }, null, this._disposables); + + this.bufferSyncSupport.onWillChange(resource => { + this.cancelInflightRequestsForResource(resource); + }); + + vscode.workspace.onDidChangeConfiguration(() => { + const oldConfiguration = this._configuration; + this._configuration = TypeScriptServiceConfiguration.loadFromWorkspace(); + + this.versionProvider.updateConfiguration(this._configuration); + this._versionManager.updateConfiguration(this._configuration); + this.pluginPathsProvider.updateConfiguration(this._configuration); + this.tracer.updateConfiguration(); + + if (this.serverState.type === ServerState.Type.Running) { + if (!this._configuration.implictProjectConfiguration.isEqualTo(oldConfiguration.implictProjectConfiguration)) { + this.setCompilerOptionsForInferredProjects(this._configuration); + } + + if (!this._configuration.isEqualTo(oldConfiguration)) { + this.restartTsServer(); + } + } + }, this, this._disposables); + + this.telemetryReporter = this._register(new VSCodeTelemetryReporter(() => { + if (this.serverState.type === ServerState.Type.Running) { + if (this.serverState.tsserverVersion) { + return this.serverState.tsserverVersion; + } + } + return this.apiVersion.fullVersionString; + })); + + this.typescriptServerSpawner = new TypeScriptServerSpawner(this.versionProvider, this._versionManager, this.logDirectoryProvider, this.pluginPathsProvider, this.logger, this.telemetryReporter, this.tracer, this.processFactory); + + this._register(this.pluginManager.onDidUpdateConfig(update => { + this.configurePlugin(update.pluginId, update.config); + })); + + this._register(this.pluginManager.onDidChangePlugins(() => { + this.restartTsServer(); + })); + } + + public get capabilities() { + if (isWeb()) { + return new ClientCapabilities( + ClientCapability.Syntax, + ClientCapability.EnhancedSyntax); + } + + if (this.apiVersion.gte(API.v400)) { + return new ClientCapabilities( + ClientCapability.Syntax, + ClientCapability.EnhancedSyntax, + ClientCapability.Semantic); + } + + return new ClientCapabilities( + ClientCapability.Syntax, + ClientCapability.Semantic); + } + + private readonly _onDidChangeCapabilities = this._register(new vscode.EventEmitter()); + readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; + + private cancelInflightRequestsForResource(resource: vscode.Uri): void { + if (this.serverState.type !== ServerState.Type.Running) { + return; + } + + for (const request of this.serverState.toCancelOnResourceChange) { + if (request.resource.toString() === resource.toString()) { + request.cancel(); + } + } + } + + public get configuration() { + return this._configuration; + } + + public dispose() { + super.dispose(); + + this.bufferSyncSupport.dispose(); + + if (this.serverState.type === ServerState.Type.Running) { + this.serverState.server.kill(); + } + + this.loadingIndicator.reset(); + } + + public restartTsServer(): void { + if (this.serverState.type === ServerState.Type.Running) { + this.info('Killing TS Server'); + this.isRestarting = true; + this.serverState.server.kill(); + } + + this.serverState = this.startService(true); + } + + private readonly _onTsServerStarted = this._register(new vscode.EventEmitter<{ version: TypeScriptVersion, usedApiVersion: API }>()); + public readonly onTsServerStarted = this._onTsServerStarted.event; + + private readonly _onDiagnosticsReceived = this._register(new vscode.EventEmitter()); + public readonly onDiagnosticsReceived = this._onDiagnosticsReceived.event; + + private readonly _onConfigDiagnosticsReceived = this._register(new vscode.EventEmitter()); + public readonly onConfigDiagnosticsReceived = this._onConfigDiagnosticsReceived.event; + + private readonly _onResendModelsRequested = this._register(new vscode.EventEmitter()); + public readonly onResendModelsRequested = this._onResendModelsRequested.event; + + private readonly _onProjectLanguageServiceStateChanged = this._register(new vscode.EventEmitter()); + public readonly onProjectLanguageServiceStateChanged = this._onProjectLanguageServiceStateChanged.event; + + private readonly _onDidBeginInstallTypings = this._register(new vscode.EventEmitter()); + public readonly onDidBeginInstallTypings = this._onDidBeginInstallTypings.event; + + private readonly _onDidEndInstallTypings = this._register(new vscode.EventEmitter()); + public readonly onDidEndInstallTypings = this._onDidEndInstallTypings.event; + + private readonly _onTypesInstallerInitializationFailed = this._register(new vscode.EventEmitter()); + public readonly onTypesInstallerInitializationFailed = this._onTypesInstallerInitializationFailed.event; + + private readonly _onSurveyReady = this._register(new vscode.EventEmitter()); + public readonly onSurveyReady = this._onSurveyReady.event; + + public get apiVersion(): API { + if (this.serverState.type === ServerState.Type.Running) { + return this.serverState.apiVersion; + } + return API.defaultVersion; + } + + public onReady(f: () => void): Promise { + return this._onReady!.promise.then(f); + } + + private info(message: string, data?: any): void { + this.logger.info(message, data); + } + + private error(message: string, data?: any): void { + this.logger.error(message, data); + } + + private logTelemetry(eventName: string, properties?: TelemetryProperties) { + this.telemetryReporter.logTelemetry(eventName, properties); + } + + private service(): ServerState.Running { + if (this.serverState.type === ServerState.Type.Running) { + return this.serverState; + } + if (this.serverState.type === ServerState.Type.Errored) { + throw this.serverState.error; + } + const newState = this.startService(); + if (newState.type === ServerState.Type.Running) { + return newState; + } + throw new Error(`Could not create TS service. Service state:${JSON.stringify(newState)}`); + } + + public ensureServiceStarted() { + if (this.serverState.type !== ServerState.Type.Running) { + this.startService(); + } + } + + private token: number = 0; + private startService(resendModels: boolean = false): ServerState.State { + this.info(`Starting TS Server `); + + if (this.isDisposed) { + this.info(`Not starting server. Disposed `); + return ServerState.None; + } + + if (this.hasServerFatallyCrashedTooManyTimes) { + this.info(`Not starting server. Too many crashes.`); + return ServerState.None; + } + + let version = this._versionManager.currentVersion; + if (!version.isValid) { + vscode.window.showWarningMessage(localize('noServerFound', 'The path {0} doesn\'t point to a valid tsserver install. Falling back to bundled TypeScript version.', version.path)); + + this._versionManager.reset(); + version = this._versionManager.currentVersion; + } + + this.info(`Using tsserver from: ${version.path}`); + + const apiVersion = version.apiVersion || API.defaultVersion; + const mytoken = ++this.token; + const handle = this.typescriptServerSpawner.spawn(version, this.capabilities, this.configuration, this.pluginManager, this.cancellerFactory, { + onFatalError: (command, err) => this.fatalError(command, err), + }); + this.serverState = new ServerState.Running(handle, apiVersion, undefined, true); + this.lastStart = Date.now(); + + /* __GDPR__ + "tsserver.spawned" : { + "${include}": [ + "${TypeScriptCommonProperties}" + ], + "localTypeScriptVersion": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "typeScriptVersionSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.logTelemetry('tsserver.spawned', { + localTypeScriptVersion: this.versionProvider.localVersion ? this.versionProvider.localVersion.displayName : '', + typeScriptVersionSource: version.source, + }); + + handle.onError((err: Error) => { + if (this.token !== mytoken) { + // this is coming from an old process + return; + } + + if (err) { + vscode.window.showErrorMessage(localize('serverExitedWithError', 'TypeScript language server exited with error. Error message is: {0}', err.message || err.name)); + } + + this.serverState = new ServerState.Errored(err, handle.tsServerLogFile); + this.error('TSServer errored with error.', err); + if (handle.tsServerLogFile) { + this.error(`TSServer log file: ${handle.tsServerLogFile}`); + } + + /* __GDPR__ + "tsserver.error" : { + "${include}": [ + "${TypeScriptCommonProperties}" + ] + } + */ + this.logTelemetry('tsserver.error'); + this.serviceExited(false); + }); + + handle.onExit((code: any) => { + if (this.token !== mytoken) { + // this is coming from an old process + return; + } + + if (code === null || typeof code === 'undefined') { + this.info('TSServer exited'); + } else { + // In practice, the exit code is an integer with no ties to any identity, + // so it can be classified as SystemMetaData, rather than CallstackOrException. + this.error(`TSServer exited with code: ${code}`); + /* __GDPR__ + "tsserver.exitWithCode" : { + "code" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "${include}": [ + "${TypeScriptCommonProperties}" + ] + } + */ + this.logTelemetry('tsserver.exitWithCode', { code: code }); + } + + if (handle.tsServerLogFile) { + this.info(`TSServer log file: ${handle.tsServerLogFile}`); + } + this.serviceExited(!this.isRestarting); + this.isRestarting = false; + }); + + handle.onEvent(event => this.dispatchEvent(event)); + + if (apiVersion.gte(API.v300) && this.capabilities.has(ClientCapability.Semantic)) { + this.loadingIndicator.startedLoadingProject(undefined /* projectName */); + } + + this.serviceStarted(resendModels); + + this._onReady!.resolve(); + this._onTsServerStarted.fire({ version: version, usedApiVersion: apiVersion }); + this._onDidChangeCapabilities.fire(); + return this.serverState; + } + + public async showVersionPicker(): Promise { + this._versionManager.promptUserForVersion(); + } + + public async openTsServerLogFile(): Promise { + if (this._configuration.tsServerLogLevel === TsServerLogLevel.Off) { + vscode.window.showErrorMessage( + localize( + 'typescript.openTsServerLog.loggingNotEnabled', + 'TS Server logging is off. Please set `typescript.tsserver.log` and restart the TS server to enable logging'), + { + title: localize( + 'typescript.openTsServerLog.enableAndReloadOption', + 'Enable logging and restart TS server'), + }) + .then(selection => { + if (selection) { + return vscode.workspace.getConfiguration().update('typescript.tsserver.log', 'verbose', true).then(() => { + this.restartTsServer(); + }); + } + return undefined; + }); + return false; + } + + if (this.serverState.type !== ServerState.Type.Running || !this.serverState.server.tsServerLogFile) { + vscode.window.showWarningMessage(localize( + 'typescript.openTsServerLog.noLogFile', + 'TS Server has not started logging.')); + return false; + } + + try { + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(this.serverState.server.tsServerLogFile)); + await vscode.window.showTextDocument(doc); + return true; + } catch { + // noop + } + + try { + await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(this.serverState.server.tsServerLogFile)); + return true; + } catch { + vscode.window.showWarningMessage(localize( + 'openTsServerLog.openFileFailedFailed', + 'Could not open TS Server log file')); + return false; + } + } + + private serviceStarted(resendModels: boolean): void { + this.bufferSyncSupport.reset(); + + const watchOptions = this.apiVersion.gte(API.v380) + ? this.configuration.watchOptions + : undefined; + + const configureOptions: Proto.ConfigureRequestArguments = { + hostInfo: 'vscode', + preferences: { + providePrefixAndSuffixTextForRename: true, + allowRenameOfImportPath: true, + includePackageJsonAutoImports: this._configuration.includePackageJsonAutoImports, + }, + watchOptions + }; + this.executeWithoutWaitingForResponse('configure', configureOptions); + this.setCompilerOptionsForInferredProjects(this._configuration); + if (resendModels) { + this._onResendModelsRequested.fire(); + this.bufferSyncSupport.reinitialize(); + this.bufferSyncSupport.requestAllDiagnostics(); + } + + // Reconfigure any plugins + for (const [config, pluginName] of this.pluginManager.configurations()) { + this.configurePlugin(config, pluginName); + } + } + + private setCompilerOptionsForInferredProjects(configuration: TypeScriptServiceConfiguration): void { + const args: Proto.SetCompilerOptionsForInferredProjectsArgs = { + options: this.getCompilerOptionsForInferredProjects(configuration) + }; + this.executeWithoutWaitingForResponse('compilerOptionsForInferredProjects', args); + } + + private getCompilerOptionsForInferredProjects(configuration: TypeScriptServiceConfiguration): Proto.ExternalProjectCompilerOptions { + return { + ...inferredProjectCompilerOptions(ProjectType.TypeScript, configuration), + allowJs: true, + allowSyntheticDefaultImports: true, + allowNonTsExtensions: true, + resolveJsonModule: true, + }; + } + + private serviceExited(restart: boolean): void { + this.loadingIndicator.reset(); + + const previousState = this.serverState; + this.serverState = ServerState.None; + + if (restart) { + const diff = Date.now() - this.lastStart; + this.numberRestarts++; + let startService = true; + + const reportIssueItem: vscode.MessageItem = { + title: localize('serverDiedReportIssue', 'Report Issue'), + }; + let prompt: Thenable | undefined = undefined; + + if (this.numberRestarts > 5) { + this.numberRestarts = 0; + if (diff < 10 * 1000 /* 10 seconds */) { + this.lastStart = Date.now(); + startService = false; + this.hasServerFatallyCrashedTooManyTimes = true; + prompt = vscode.window.showErrorMessage( + localize('serverDiedAfterStart', 'The TypeScript language service died 5 times right after it got started. The service will not be restarted.'), + reportIssueItem); + + /* __GDPR__ + "serviceExited" : { + "${include}": [ + "${TypeScriptCommonProperties}" + ] + } + */ + this.logTelemetry('serviceExited'); + } else if (diff < 60 * 1000 * 5 /* 5 Minutes */) { + this.lastStart = Date.now(); + prompt = vscode.window.showWarningMessage( + localize('serverDied', 'The TypeScript language service died unexpectedly 5 times in the last 5 Minutes.'), + reportIssueItem); + } + } else if (['vscode-insiders', 'code-oss'].includes(vscode.env.uriScheme)) { + // Prompt after a single restart + if (!this._isPromptingAfterCrash && previousState.type === ServerState.Type.Errored && previousState.error instanceof TypeScriptServerError) { + this.numberRestarts = 0; + this._isPromptingAfterCrash = true; + prompt = vscode.window.showWarningMessage( + localize('serverDiedOnce', 'The TypeScript language service died unexpectedly.'), + reportIssueItem); + } + } + + prompt?.then(item => { + this._isPromptingAfterCrash = false; + + if (item === reportIssueItem) { + const args = previousState.type === ServerState.Type.Errored && previousState.error instanceof TypeScriptServerError + ? getReportIssueArgsForError(previousState.error, previousState.tsServerLogFile) + : undefined; + vscode.commands.executeCommand('workbench.action.openIssueReporter', args); + } + }); + + if (startService) { + this.startService(true); + } + } + } + + public normalizedPath(resource: vscode.Uri): string | undefined { + if (fileSchemes.disabledSchemes.has(resource.scheme)) { + return undefined; + } + + switch (resource.scheme) { + case fileSchemes.file: + { + let result = resource.fsPath; + if (!result) { + return undefined; + } + result = path.normalize(result); + + // Both \ and / must be escaped in regular expressions + return result.replace(new RegExp('\\' + this.pathSeparator, 'g'), '/'); + } + default: + { + return this.inMemoryResourcePrefix + resource.toString(true); + } + } + } + + public toPath(resource: vscode.Uri): string | undefined { + return this.normalizedPath(resource); + } + + public toOpenedFilePath(document: vscode.TextDocument, options: { suppressAlertOnFailure?: boolean } = {}): string | undefined { + if (!this.bufferSyncSupport.ensureHasBuffer(document.uri)) { + if (!options.suppressAlertOnFailure && !fileSchemes.disabledSchemes.has(document.uri.scheme)) { + console.error(`Unexpected resource ${document.uri}`); + } + return undefined; + } + return this.toPath(document.uri); + } + + public hasCapabilityForResource(resource: vscode.Uri, capability: ClientCapability): boolean { + switch (capability) { + case ClientCapability.Semantic: + { + return fileSchemes.semanticSupportedSchemes.includes(resource.scheme); + } + case ClientCapability.Syntax: + case ClientCapability.EnhancedSyntax: + { + return true; + } + } + } + + public toResource(filepath: string): vscode.Uri { + if (isWeb()) { + // On web, treat absolute paths as pointing to standard lib files + if (filepath.startsWith('/')) { + // below codes are changed by github1s + return vscode.Uri.joinPath(this.context.extensionUri, 'dist', 'browser', 'typescript', filepath.slice(1)); + // about codes are changed by github1s + } + } + + if (filepath.startsWith(this.inMemoryResourcePrefix)) { + const resource = vscode.Uri.parse(filepath.slice(1)); + return this.bufferSyncSupport.toVsCodeResource(resource); + } + return this.bufferSyncSupport.toResource(filepath); + } + + public getWorkspaceRootForResource(resource: vscode.Uri): string | undefined { + const roots = vscode.workspace.workspaceFolders ? Array.from(vscode.workspace.workspaceFolders) : undefined; + if (!roots || !roots.length) { + return undefined; + } + + if (resource.scheme === fileSchemes.file || resource.scheme === fileSchemes.untitled) { + for (const root of roots.sort((a, b) => a.uri.fsPath.length - b.uri.fsPath.length)) { + if (resource.fsPath.startsWith(root.uri.fsPath + path.sep)) { + return root.uri.fsPath; + } + } + return roots[0].uri.fsPath; + } + + return undefined; + } + + public execute(command: keyof TypeScriptRequests, args: any, token: vscode.CancellationToken, config?: ExecConfig): Promise> { + let execution: Promise>; + + if (config?.cancelOnResourceChange) { + const runningServerState = this.service(); + + const source = new vscode.CancellationTokenSource(); + token.onCancellationRequested(() => source.cancel()); + + const inFlight: ToCancelOnResourceChanged = { + resource: config.cancelOnResourceChange, + cancel: () => source.cancel(), + }; + runningServerState.toCancelOnResourceChange.add(inFlight); + + execution = this.executeImpl(command, args, { + isAsync: false, + token: source.token, + expectsResult: true, + ...config, + }).finally(() => { + runningServerState.toCancelOnResourceChange.delete(inFlight); + source.dispose(); + }); + } else { + execution = this.executeImpl(command, args, { + isAsync: false, + token, + expectsResult: true, + ...config, + }); + } + + if (config?.nonRecoverable) { + execution.catch(err => this.fatalError(command, err)); + } + + return execution; + } + + public executeWithoutWaitingForResponse(command: keyof TypeScriptRequests, args: any): void { + this.executeImpl(command, args, { + isAsync: false, + token: undefined, + expectsResult: false + }); + } + + public executeAsync(command: keyof TypeScriptRequests, args: Proto.GeterrRequestArgs, token: vscode.CancellationToken): Promise> { + return this.executeImpl(command, args, { + isAsync: true, + token, + expectsResult: true + }); + } + + private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: false, lowPriority?: boolean, requireSemantic?: boolean }): undefined; + private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, requireSemantic?: boolean }): Promise>; + private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean, token?: vscode.CancellationToken, expectsResult: boolean, lowPriority?: boolean, requireSemantic?: boolean }): Promise> | undefined { + this.bufferSyncSupport.beforeCommand(command); + const runningServerState = this.service(); + return runningServerState.server.executeImpl(command, args, executeInfo); + } + + public interruptGetErr(f: () => R): R { + return this.bufferSyncSupport.interruptGetErr(f); + } + + private fatalError(command: string, error: unknown): void { + /* __GDPR__ + "fatalError" : { + "${include}": [ + "${TypeScriptCommonProperties}", + "${TypeScriptRequestErrorProperties}" + ], + "command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.logTelemetry('fatalError', { ...(error instanceof TypeScriptServerError ? error.telemetry : { command }) }); + console.error(`A non-recoverable error occured while executing tsserver command: ${command}`); + if (error instanceof TypeScriptServerError && error.serverErrorText) { + console.error(error.serverErrorText); + } + + if (this.serverState.type === ServerState.Type.Running) { + this.info('Killing TS Server'); + const logfile = this.serverState.server.tsServerLogFile; + this.serverState.server.kill(); + if (error instanceof TypeScriptServerError) { + this.serverState = new ServerState.Errored(error, logfile); + } + } + } + + private dispatchEvent(event: Proto.Event) { + switch (event.event) { + case EventName.syntaxDiag: + case EventName.semanticDiag: + case EventName.suggestionDiag: + // This event also roughly signals that projects have been loaded successfully (since the TS server is synchronous) + this.loadingIndicator.reset(); + + const diagnosticEvent = event as Proto.DiagnosticEvent; + if (diagnosticEvent.body && diagnosticEvent.body.diagnostics) { + this._onDiagnosticsReceived.fire({ + kind: getDignosticsKind(event), + resource: this.toResource(diagnosticEvent.body.file), + diagnostics: diagnosticEvent.body.diagnostics + }); + } + break; + + case EventName.configFileDiag: + this._onConfigDiagnosticsReceived.fire(event as Proto.ConfigFileDiagnosticEvent); + break; + + case EventName.telemetry: + { + const body = (event as Proto.TelemetryEvent).body; + this.dispatchTelemetryEvent(body); + break; + } + case EventName.projectLanguageServiceState: + { + const body = (event as Proto.ProjectLanguageServiceStateEvent).body!; + if (this.serverState.type === ServerState.Type.Running) { + this.serverState.updateLanguageServiceEnabled(body.languageServiceEnabled); + } + this._onProjectLanguageServiceStateChanged.fire(body); + break; + } + case EventName.projectsUpdatedInBackground: + this.loadingIndicator.reset(); + + const body = (event as Proto.ProjectsUpdatedInBackgroundEvent).body; + const resources = body.openFiles.map(file => this.toResource(file)); + this.bufferSyncSupport.getErr(resources); + break; + + case EventName.beginInstallTypes: + this._onDidBeginInstallTypings.fire((event as Proto.BeginInstallTypesEvent).body); + break; + + case EventName.endInstallTypes: + this._onDidEndInstallTypings.fire((event as Proto.EndInstallTypesEvent).body); + break; + + case EventName.typesInstallerInitializationFailed: + this._onTypesInstallerInitializationFailed.fire((event as Proto.TypesInstallerInitializationFailedEvent).body); + break; + + case EventName.surveyReady: + this._onSurveyReady.fire((event as Proto.SurveyReadyEvent).body); + break; + + case EventName.projectLoadingStart: + this.loadingIndicator.startedLoadingProject((event as Proto.ProjectLoadingStartEvent).body.projectName); + break; + + case EventName.projectLoadingFinish: + this.loadingIndicator.finishedLoadingProject((event as Proto.ProjectLoadingFinishEvent).body.projectName); + break; + } + } + + private dispatchTelemetryEvent(telemetryData: Proto.TelemetryEventBody): void { + const properties: { [key: string]: string } = Object.create(null); + switch (telemetryData.telemetryEventName) { + case 'typingsInstalled': + const typingsInstalledPayload: Proto.TypingsInstalledTelemetryEventPayload = (telemetryData.payload as Proto.TypingsInstalledTelemetryEventPayload); + properties['installedPackages'] = typingsInstalledPayload.installedPackages; + + if (typeof typingsInstalledPayload.installSuccess === 'boolean') { + properties['installSuccess'] = typingsInstalledPayload.installSuccess.toString(); + } + if (typeof typingsInstalledPayload.typingsInstallerVersion === 'string') { + properties['typingsInstallerVersion'] = typingsInstalledPayload.typingsInstallerVersion; + } + break; + + default: + const payload = telemetryData.payload; + if (payload) { + Object.keys(payload).forEach((key) => { + try { + if (payload.hasOwnProperty(key)) { + properties[key] = typeof payload[key] === 'string' ? payload[key] : JSON.stringify(payload[key]); + } + } catch (e) { + // noop + } + }); + } + break; + } + if (telemetryData.telemetryEventName === 'projectInfo') { + if (this.serverState.type === ServerState.Type.Running) { + this.serverState.updateTsserverVersion(properties['version']); + } + } + + /* __GDPR__ + "typingsInstalled" : { + "installedPackages" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, + "installSuccess": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "typingsInstallerVersion": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, + "${include}": [ + "${TypeScriptCommonProperties}" + ] + } + */ + // __GDPR__COMMENT__: Other events are defined by TypeScript. + this.logTelemetry(telemetryData.telemetryEventName, properties); + } + + private configurePlugin(pluginName: string, configuration: {}): any { + if (this.apiVersion.gte(API.v314)) { + this.executeWithoutWaitingForResponse('configurePlugin', { pluginName, configuration }); + } + } +} + +function getReportIssueArgsForError( + error: TypeScriptServerError, + logPath: string | undefined, +): { extensionId: string, issueTitle: string, issueBody: string } | undefined { + if (!error.serverStack || !error.serverMessage) { + return undefined; + } + + // Note these strings are intentionally not localized + // as we want users to file issues in english + + const sections = [ + `❗️❗️❗️ Please fill in the sections below to help us diagnose the issue ❗️❗️❗️`, + `**TypeScript Version:** ${error.version.apiVersion?.fullVersionString}`, + `**Steps to reproduce crash** + +1. +2. +3.`, + ]; + + if (logPath) { + sections.push(`**TS Server Log** + +❗️ Please review and upload this log file to help us diagnose this crash: + +\`${logPath}\` + +The log file may contain personal data, including full paths and source code from your workspace. You can scrub the log file to remove paths or other personal information. +`); + } else { + + sections.push(`**TS Server Log** + +❗️Server logging disabled. To help us fix crashes like this, please enable logging by setting: + +\`\`\`json +"typescript.tsserver.log": "verbose" +\`\`\` + +After enabling this setting, future crash reports will include the server log.`); + } + + sections.push(`**TS Server Error Stack** + +Server: \`${error.serverId}\` + +\`\`\` +${error.serverStack} +\`\`\``); + + return { + extensionId: 'vscode.typescript-language-features', + issueTitle: `TS Server fatal error: ${error.serverMessage}`, + + issueBody: sections.join('\n\n') + }; +} + +function getDignosticsKind(event: Proto.Event) { + switch (event.event) { + case 'syntaxDiag': return DiagnosticKind.Syntax; + case 'semanticDiag': return DiagnosticKind.Semantic; + case 'suggestionDiag': return DiagnosticKind.Suggestion; + } + throw new Error('Unknown dignostics kind'); +} + +class ServerInitializingIndicator extends Disposable { + private _task?: { project: string | undefined, resolve: () => void, reject: () => void }; + + public reset(): void { + if (this._task) { + this._task.reject(); + this._task = undefined; + } + } + + /** + * Signal that a project has started loading. + */ + public startedLoadingProject(projectName: string | undefined): void { + // TS projects are loaded sequentially. Cancel existing task because it should always be resolved before + // the incoming project loading task is. + this.reset(); + + vscode.window.withProgress({ + location: vscode.ProgressLocation.Window, + title: localize('serverLoading.progress', "Initializing JS/TS language features"), + }, () => new Promise((resolve, reject) => { + this._task = { project: projectName, resolve, reject }; + })); + } + + public finishedLoadingProject(projectName: string | undefined): void { + if (this._task && this._task.project === projectName) { + this._task.resolve(); + this._task = undefined; + } + } +} + diff --git a/vscode-web-github1s/package.json b/vscode-web-github1s/package.json index 4a3b604b9..fd5009021 100644 --- a/vscode-web-github1s/package.json +++ b/vscode-web-github1s/package.json @@ -1,6 +1,6 @@ { "name": "@github1s/vscode-web", - "version": "0.1.8", + "version": "0.1.10", "description": "VS Code web for GitHub1s", "author": "github1s", "license": "MIT", diff --git a/vscode-web-github1s/scripts/sync-code.sh b/vscode-web-github1s/scripts/sync-code.sh index e1420f0d3..23a393f13 100755 --- a/vscode-web-github1s/scripts/sync-code.sh +++ b/vscode-web-github1s/scripts/sync-code.sh @@ -9,6 +9,7 @@ echo $APP_ROOT function main() { cd ${APP_ROOT} rsync -a src/ lib/vscode/src + rsync -a extensions/ lib/vscode/extensions } main "$@" diff --git a/vscode-web-github1s/src/vs/base/common/platform.ts b/vscode-web-github1s/src/vs/base/common/platform.ts index b2f83e9b0..9820e719b 100644 --- a/vscode-web-github1s/src/vs/base/common/platform.ts +++ b/vscode-web-github1s/src/vs/base/common/platform.ts @@ -104,9 +104,11 @@ if (typeof navigator === 'object' && !isElectronRenderer) { _isLinux = _userAgent.indexOf('Linux') >= 0; _isWeb = true; // below codes are changed by github1s + // `window` may can not be accessed in webworker (for example opera) + const _userAgentStr: string = (typeof navigator !== 'undefined' ? (navigator.userAgent || (navigator as any).vendor) : undefined) || (typeof window !== 'undefined' ? (window as any).opera : undefined); // this code is from http://detectmobilebrowsers.com/ // eslint-disable-next-line - _isMobile = (function (a) { return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4)) })(navigator.userAgent || (navigator as any).vendor || (window as any).opera); + _isMobile = (function (a) { return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4)) })(_userAgentStr || ''); // above codes are changed by github1s _locale = navigator.language; _language = _locale; diff --git a/yarn.lock b/yarn.lock index 45d78f632..bae94c9ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -39,10 +39,10 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@github1s/vscode-web@0.1.8": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@github1s/vscode-web/-/vscode-web-0.1.8.tgz#85305b867a86c8308e9200d0ac2fa8403b66377e" - integrity sha512-dOzlbpUL+pLqqb6DtH7WwVmlRqzSpY0NXOxbJCg5jHu8XsrLsV1cIRDcnmLk16EsNmeU2egtgPGZsYPhwL2feg== +"@github1s/vscode-web@0.1.10": + version "0.1.10" + resolved "https://registry.yarnpkg.com/@github1s/vscode-web/-/vscode-web-0.1.10.tgz#65237bd929aa4426cbf10b28239444170b53cd03" + integrity sha512-q8xTuE1MlKyUsBtQG6+mpQKVF4cohKCbqvHoqiIDalCP+XFdeBTWvTMnHnn+x/36a+Ae+1dzMqQGxo1/kq9qOA== dependencies: iconv-lite-umd "0.6.8" jschardet "2.2.1" @@ -938,9 +938,9 @@ get-stream@^5.0.0: pump "^3.0.0" glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1"