From a7562668e3dde415c8557b48ea7cabb8d43e7118 Mon Sep 17 00:00:00 2001 From: netcon Date: Wed, 5 Aug 2026 22:17:54 +0800 Subject: [PATCH 1/9] feat: github/gitlab oauth with state validation (#709) --- .github/workflows/test-wtih-vscode-build.yml | 2 +- functions/api/github-auth-callback.ts | 14 +++++++++---- functions/api/gitlab-auth-callback.ts | 19 ++++++++++------- scripts/build.js | 1 - src/github-auth.ts | 22 ++++++++++---------- src/gitlab-auth.ts | 17 +++++++-------- src/global.d.ts | 2 ++ webpack.config.js | 2 ++ 8 files changed, 46 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test-wtih-vscode-build.yml b/.github/workflows/test-wtih-vscode-build.yml index 5bc2b0d10..cc4fc5409 100644 --- a/.github/workflows/test-wtih-vscode-build.yml +++ b/.github/workflows/test-wtih-vscode-build.yml @@ -9,7 +9,7 @@ on: - master jobs: - build: + build-with-vscode-build: strategy: matrix: os: [macos-14] diff --git a/functions/api/github-auth-callback.ts b/functions/api/github-auth-callback.ts index 34818e035..90f910f0f 100644 --- a/functions/api/github-auth-callback.ts +++ b/functions/api/github-auth-callback.ts @@ -18,10 +18,14 @@ const createResponseHtml = (text: string, script: string) => ` // return the data to the opener window by postMessage API, // and close current window if successfully connected -const createAuthorizeResultHtml = (data: Record, origins: string) => { +const createAuthorizeResultHtml = (data: Record, state: string, origins: string) => { const errorText = 'Failed! You can close this window and retry.'; const successText = 'Connected! You can now close this window.'; - const resultStr = `{ type: 'authorizing', payload: ${JSON.stringify(data)} }`; + const resultStr = JSON.stringify({ + type: 'authorizing', + payload: data, + state: state.replace(/[^a-zA-Z0-9]/g, ''), + }).replace(/ = async ({ request, env }) => { - const code = new URL(request.url).searchParams.get('code'); + const searchParams = new URL(request.url).searchParams; + const code = searchParams.get('code'); const createResponse = (status, data) => { - const body = createAuthorizeResultHtml(data, env.GITHUB1S_ALLOWED_ORIGINS); + const state = searchParams.get('state') || ''; + const body = createAuthorizeResultHtml(data, state, env.GITHUB1S_ALLOWED_ORIGINS); return new Response(body, { status, headers: { 'content-type': 'text/html' } }); }; diff --git a/functions/api/gitlab-auth-callback.ts b/functions/api/gitlab-auth-callback.ts index 46143f37d..e8c5bba5d 100644 --- a/functions/api/gitlab-auth-callback.ts +++ b/functions/api/gitlab-auth-callback.ts @@ -3,8 +3,6 @@ * @author netcon */ -const AUTH_REDIRECT_URI = 'https://auth.gitlab1s.com/api/gitlab-auth-callback'; - const createResponseHtml = (text: string, script: string) => ` @@ -20,10 +18,14 @@ const createResponseHtml = (text: string, script: string) => ` // return the data to the opener window by postMessage API, // and close current window if successfully connected -const createAuthorizeResultHtml = (data: Record, origins: string) => { +const createAuthorizeResultHtml = (data: Record, state: string, origins: string) => { const errorText = 'Failed! You can close this window and retry.'; const successText = 'Connected! You can now close this window.'; - const resultStr = `{ type: 'authorizing', payload: ${JSON.stringify(data)} }`; + const resultStr = JSON.stringify({ + type: 'authorizing', + payload: data, + state: state.replace(/[^a-zA-Z0-9]/g, ''), + }).replace(/ = async ({ request, env }) => { - const code = new URL(request.url).searchParams.get('code'); + const searchParams = new URL(request.url).searchParams; + const code = searchParams.get('code'); const createResponse = (status, data) => { - const body = createAuthorizeResultHtml(data, env.GITLAB1S_ALLOWED_ORIGINS); + const state = searchParams.get('state') || ''; + const body = createAuthorizeResultHtml(data, state, env.GITLAB1S_ALLOWED_ORIGINS); return new Response(body, { status, headers: { 'content-type': 'text/html' } }); }; @@ -65,7 +70,7 @@ export const onRequest: PagesFunction<{ code, client_id: env.GITLAB_OAUTH_ID, client_secret: env.GITLAB_OAUTH_SECRET, - redirect_uri: AUTH_REDIRECT_URI, + redirect_uri: env.GITLAB_OAUTH_REDIRECT_URI, grant_type: 'authorization_code', }), headers: { accept: 'application/json', 'content-type': 'application/json' }, diff --git a/scripts/build.js b/scripts/build.js index 5d424fc84..d19600d04 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -2,7 +2,6 @@ import path from 'path'; import fs from 'fs-extra'; -import cp from 'child_process'; import { executeCommand, PROJECT_ROOT } from './utils.js'; const main = () => { diff --git a/src/github-auth.ts b/src/github-auth.ts index 23b558122..86acf6daf 100644 --- a/src/github-auth.ts +++ b/src/github-auth.ts @@ -4,23 +4,22 @@ */ const GITHUB_ORIGIN = 'https://github.com'; -const AUTH_PAGE_ORIGIN = 'https://auth.github1s.com'; -const AUTH_REDIRECT_URI = `${AUTH_PAGE_ORIGIN}/api/github-auth-callback`; -const CLIENT_ID = 'eae6621348403ea49103'; +const OAUTH_REDIRECT_URI = `${location.origin}/api/github-auth-callback`; const OPEN_WINDOW_FEATURES = 'directories=no,titlebar=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=no,width=800,height=520,top=150,left=150'; -export const createRandomString = (length: number) => { - const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - return Array.from({ length }, () => charset.charAt(Math.floor(Math.random() * charset.length))).join(''); +export const createOAuthState = () => { + const bytes = new Uint8Array(16); + window.crypto.getRandomValues(bytes); + return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(''); }; const createAuthorizeUrl = (state: string) => { const parameters = Object.entries({ state, scope: 'repo,user:email', - client_id: CLIENT_ID, - redirect_uri: AUTH_REDIRECT_URI, + client_id: GITHUB_OAUTH_ID, + redirect_uri: OAUTH_REDIRECT_URI, }).map(([key, value]) => `${key}=${encodeURIComponent(value)}`); return `${GITHUB_ORIGIN}/login/oauth/authorize?${parameters.join('&')}`; }; @@ -28,7 +27,7 @@ const createAuthorizeUrl = (state: string) => { export const timeout = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export const ConnectToGitHub = () => { - const STATE = createRandomString(32); + const STATE = createOAuthState(); const opener = window.open(createAuthorizeUrl(STATE), '_blank', OPEN_WINDOW_FEATURES); return new Promise((resolve) => { @@ -37,9 +36,10 @@ export const ConnectToGitHub = () => { // the user can be still open it from the tip. In this case, the `opener` // is null, and we should still process the authorizing message const isValidOpener = !!(opener && event.source === opener); - const isValidOrigin = event.origin === AUTH_PAGE_ORIGIN; + const isValidOrigin = event.origin === location.origin; const isValidResponse = event.data ? event.data.type === 'authorizing' : false; - if (!isValidOpener || !isValidOrigin || !isValidResponse) { + const isValidState = event.data ? event.data.state === STATE : false; + if (!isValidOpener || !isValidOrigin || !isValidResponse || !isValidState) { return; } window.removeEventListener('message', handleAuthMessage); diff --git a/src/gitlab-auth.ts b/src/gitlab-auth.ts index 6fe80a240..661579031 100644 --- a/src/gitlab-auth.ts +++ b/src/gitlab-auth.ts @@ -3,12 +3,10 @@ * @author netcon */ -import { timeout, createRandomString } from './github-auth'; +import { timeout, createOAuthState } from './github-auth'; const GITLAB_ORIGIN = 'https://gitlab.com'; -const AUTH_PAGE_ORIGIN = 'https://auth.gitlab1s.com'; -const AUTH_REDIRECT_URI = 'https://auth.gitlab1s.com/api/gitlab-auth-callback'; -const CLIENT_ID = '5ef142320efe9d2e8caeb0185771bb126d3035dc0a325c6ad5bab567f320d564'; +const OAUTH_REDIRECT_URI = `${location.origin}/api/gitlab-auth-callback`; const OPEN_WINDOW_FEATURES = 'directories=no,titlebar=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=no,width=800,height=520,top=150,left=150'; @@ -17,15 +15,15 @@ const createAuthorizeUrl = (state: string) => { state, scope: 'read_api', response_type: 'code', - client_id: CLIENT_ID, - redirect_uri: AUTH_REDIRECT_URI, + client_id: GITLAB_OAUTH_ID, + redirect_uri: OAUTH_REDIRECT_URI, }).map(([key, value]) => `${key}=${encodeURIComponent(value)}`); return `${GITLAB_ORIGIN}/oauth/authorize?${parameters.join('&')}`; }; // https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow export const ConnectToGitLab = async () => { - const STATE = createRandomString(32); + const STATE = createOAuthState(); const opener = window.open(createAuthorizeUrl(STATE), '_blank', OPEN_WINDOW_FEATURES); return new Promise((resolve) => { @@ -34,9 +32,10 @@ export const ConnectToGitLab = async () => { // the user can be still open it from the tip. In this case, the `opener` // is null, and we should still process the authorizing message const isValidOpener = !!(opener && event.source === opener); - const isValidOrigin = event.origin === AUTH_PAGE_ORIGIN; + const isValidOrigin = event.origin === location.origin; const isValidResponse = event.data ? event.data.type === 'authorizing' : false; - if (!isValidOpener || !isValidOrigin || !isValidResponse) { + const isValidState = event.data ? event.data.state === STATE : false; + if (!isValidOpener || !isValidOrigin || !isValidResponse || !isValidState) { return; } window.removeEventListener('message', handleAuthMessage); diff --git a/src/global.d.ts b/src/global.d.ts index db3ffabed..07f17951c 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -11,6 +11,8 @@ declare const GITHUB_ORIGIN: string; declare const GITLAB_ORIGIN: string; declare const GITHUB1S_EXTENSIONS: string; declare const AVAILABLE_LANGUAGES: string[]; +declare const GITHUB_OAUTH_ID: string; +declare const GITLAB_OAUTH_ID: string; /* eslint-disable no-var */ declare var dynamicImport: (url: string) => Promise; diff --git a/webpack.config.js b/webpack.config.js index 01a7315dc..1df4501a5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -87,6 +87,8 @@ export default (env, argv) => { GITLAB_ORIGIN: JSON.stringify(process.env.GITLAB_DOMAIN || 'https://gitlab.com'), GITHUB1S_EXTENSIONS: JSON.stringify(packUtils.getBuiltinExtensions(devVscode)), AVAILABLE_LANGUAGES: JSON.stringify(availableLanguages), + GITHUB_OAUTH_ID: JSON.stringify(process.env.GITHUB_OAUTH_ID || ''), + GITLAB_OAUTH_ID: JSON.stringify(process.env.GITLAB_OAUTH_ID || ''), }), ], performance: false, From 426f0e9a36437273dfd01bada61a56295d7f3d74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 23:26:58 +0800 Subject: [PATCH 2/9] chore(deps-dev): bump fast-uri from 3.1.4 to 3.1.5 (#710) Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.4 to 3.1.5. - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](https://github.com/fastify/fast-uri/compare/v3.1.4...v3.1.5) --- updated-dependencies: - dependency-name: fast-uri dependency-version: 3.1.5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3af5c6c70..d77cffa34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3595,9 +3595,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", - "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "dev": true, "funding": [ { From 656b4be8621a935641728d4d45af81dd26860bf1 Mon Sep 17 00:00:00 2001 From: netcon Date: Thu, 6 Aug 2026 11:19:32 +0800 Subject: [PATCH 3/9] feat: fit COOP headers (#711) --- functions/api/github-auth-callback.ts | 68 +++++++++--------------- functions/api/gitlab-auth-callback.ts | 63 +++++++--------------- package.json | 1 + public/_headers | 3 ++ src/github-auth.ts | 37 ++----------- src/gitlab-auth.ts | 27 +--------- src/oauth-callback.ts | 58 ++++++++++++++++++++ src/oauth-common.ts | 3 ++ src/oauth-web.ts | 76 +++++++++++++++++++++++++++ webpack.config.js | 1 + 10 files changed, 192 insertions(+), 145 deletions(-) create mode 100644 public/_headers create mode 100644 src/oauth-callback.ts create mode 100644 src/oauth-common.ts create mode 100644 src/oauth-web.ts diff --git a/functions/api/github-auth-callback.ts b/functions/api/github-auth-callback.ts index 90f910f0f..408dba46f 100644 --- a/functions/api/github-auth-callback.ts +++ b/functions/api/github-auth-callback.ts @@ -3,57 +3,27 @@ * @author netcon */ -const createResponseHtml = (text: string, script: string) => ` - - - - Connect to GitHub - - -

${text}

- - - -`; - -// return the data to the opener window by postMessage API, -// and close current window if successfully connected -const createAuthorizeResultHtml = (data: Record, state: string, origins: string) => { - const errorText = 'Failed! You can close this window and retry.'; - const successText = 'Connected! You can now close this window.'; - const resultStr = JSON.stringify({ - type: 'authorizing', - payload: data, - state: state.replace(/[^a-zA-Z0-9]/g, ''), - }).replace(/ window.close(), 50);'}`; - return createResponseHtml(data.error ? errorText : successText, script); -}; - -const MISSING_CODE_ERROR = { - error: 'request_invalid', - error_description: 'Missing code', -}; -const UNKNOWN_ERROR = { - error: 'internal_error', - error_description: 'Unknown error', -}; +import { + createAuthorizeResultHtml, + INVALID_ORIGIN_ERROR, + MISSING_CODE_ERROR, + UNKNOWN_ERROR, +} from '../../src/oauth-callback'; export const onRequest: PagesFunction<{ GITHUB_OAUTH_ID: string; GITHUB_OAUTH_SECRET: string; GITHUB1S_ALLOWED_ORIGINS: string; }> = async ({ request, env }) => { - const searchParams = new URL(request.url).searchParams; + const { searchParams, origin } = new URL(request.url); const code = searchParams.get('code'); + const allowedOrigins = env.GITHUB1S_ALLOWED_ORIGINS.split(',') + .map((item) => item.trim()) + .filter(Boolean); - const createResponse = (status, data) => { + const createResponse = (status: number, data: Record) => { const state = searchParams.get('state') || ''; - const body = createAuthorizeResultHtml(data, state, env.GITHUB1S_ALLOWED_ORIGINS); + const body = createAuthorizeResultHtml('Connect to GitHub', data, state, origin); return new Response(body, { status, headers: { 'content-type': 'text/html' } }); }; @@ -61,14 +31,24 @@ export const onRequest: PagesFunction<{ return createResponse(401, MISSING_CODE_ERROR); } + if (!allowedOrigins.includes(origin)) { + return createResponse(401, INVALID_ORIGIN_ERROR); + } + try { // https://docs.github.com/en/developers/apps/authorizing-oauth-apps#2-users-are-redirected-back-to-your-site-by-github const response = await fetch('https://github.com/login/oauth/access_token', { method: 'POST', - body: JSON.stringify({ client_id: env.GITHUB_OAUTH_ID, client_secret: env.GITHUB_OAUTH_SECRET, code }), + body: JSON.stringify({ + code, + client_id: env.GITHUB_OAUTH_ID, + client_secret: env.GITHUB_OAUTH_SECRET, + redirect_uri: `${origin}/api/github-auth-callback`, + }), headers: { accept: 'application/json', 'content-type': 'application/json' }, }); - return response.json().then((result) => createResponse(response.status, result)); + const result = (await response.json()) as Record; + return createResponse(response.status, result); } catch (e) { return createResponse(500, UNKNOWN_ERROR); } diff --git a/functions/api/gitlab-auth-callback.ts b/functions/api/gitlab-auth-callback.ts index e8c5bba5d..f20159252 100644 --- a/functions/api/gitlab-auth-callback.ts +++ b/functions/api/gitlab-auth-callback.ts @@ -3,45 +3,12 @@ * @author netcon */ -const createResponseHtml = (text: string, script: string) => ` - - - - Connect to GitLab - - -

${text}

- - - -`; - -// return the data to the opener window by postMessage API, -// and close current window if successfully connected -const createAuthorizeResultHtml = (data: Record, state: string, origins: string) => { - const errorText = 'Failed! You can close this window and retry.'; - const successText = 'Connected! You can now close this window.'; - const resultStr = JSON.stringify({ - type: 'authorizing', - payload: data, - state: state.replace(/[^a-zA-Z0-9]/g, ''), - }).replace(/ window.close(), 50);'}`; - return createResponseHtml(data.error ? errorText : successText, script); -}; - -const MISSING_CODE_ERROR = { - error: 'request_invalid', - error_description: 'Missing code', -}; -const UNKNOWN_ERROR = { - error: 'internal_error', - error_description: 'Unknown error', -}; +import { + createAuthorizeResultHtml, + INVALID_ORIGIN_ERROR, + MISSING_CODE_ERROR, + UNKNOWN_ERROR, +} from '../../src/oauth-callback'; export const onRequest: PagesFunction<{ GITLAB_OAUTH_ID: string; @@ -49,12 +16,15 @@ export const onRequest: PagesFunction<{ GITLAB1S_ALLOWED_ORIGINS: string; GITLAB_OAUTH_REDIRECT_URI: string; }> = async ({ request, env }) => { - const searchParams = new URL(request.url).searchParams; + const { searchParams, origin } = new URL(request.url); const code = searchParams.get('code'); + const allowedOrigins = env.GITLAB1S_ALLOWED_ORIGINS.split(',') + .map((item) => item.trim()) + .filter(Boolean); - const createResponse = (status, data) => { + const createResponse = (status: number, data: Record) => { const state = searchParams.get('state') || ''; - const body = createAuthorizeResultHtml(data, state, env.GITLAB1S_ALLOWED_ORIGINS); + const body = createAuthorizeResultHtml('Connect to GitLab', data, state, origin); return new Response(body, { status, headers: { 'content-type': 'text/html' } }); }; @@ -62,6 +32,10 @@ export const onRequest: PagesFunction<{ return createResponse(401, MISSING_CODE_ERROR); } + if (!allowedOrigins.includes(origin)) { + return createResponse(401, INVALID_ORIGIN_ERROR); + } + try { // https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow const response = await fetch('https://gitlab.com/oauth/token', { @@ -70,12 +44,13 @@ export const onRequest: PagesFunction<{ code, client_id: env.GITLAB_OAUTH_ID, client_secret: env.GITLAB_OAUTH_SECRET, - redirect_uri: env.GITLAB_OAUTH_REDIRECT_URI, + redirect_uri: `${origin}/api/gitlab-auth-callback`, grant_type: 'authorization_code', }), headers: { accept: 'application/json', 'content-type': 'application/json' }, }); - return response.json().then((result) => createResponse(response.status, result)); + const result = (await response.json()) as Record; + return createResponse(response.status, result); } catch (e) { return createResponse(500, UNKNOWN_ERROR); } diff --git a/package.json b/package.json index 264949427..b5a1e6605 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "link": "node scripts/link.js", "format": "prettier --write .", "eslint": "eslint --fix", + "typecheck": "tsc --noEmit && tsc --noEmit -p functions/tsconfig.json", "test:ci": "start-test watch:dev-server 8080 test", "test": "cd tests && npm install && npx playwright install && npm run test", "postinstall": "husky install && node scripts/postinstall.js" diff --git a/public/_headers b/public/_headers new file mode 100644 index 000000000..3a270de9d --- /dev/null +++ b/public/_headers @@ -0,0 +1,3 @@ +/* + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: credentialless diff --git a/src/github-auth.ts b/src/github-auth.ts index 86acf6daf..c5956dbf9 100644 --- a/src/github-auth.ts +++ b/src/github-auth.ts @@ -3,17 +3,15 @@ * @author netcon */ +import { createOAuthState, waitForOAuthResult } from './oauth-web'; + +export { createOAuthState } from './oauth-web'; + const GITHUB_ORIGIN = 'https://github.com'; const OAUTH_REDIRECT_URI = `${location.origin}/api/github-auth-callback`; const OPEN_WINDOW_FEATURES = 'directories=no,titlebar=no,toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=no,width=800,height=520,top=150,left=150'; -export const createOAuthState = () => { - const bytes = new Uint8Array(16); - window.crypto.getRandomValues(bytes); - return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(''); -}; - const createAuthorizeUrl = (state: string) => { const parameters = Object.entries({ state, @@ -24,33 +22,8 @@ const createAuthorizeUrl = (state: string) => { return `${GITHUB_ORIGIN}/login/oauth/authorize?${parameters.join('&')}`; }; -export const timeout = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - export const ConnectToGitHub = () => { const STATE = createOAuthState(); const opener = window.open(createAuthorizeUrl(STATE), '_blank', OPEN_WINDOW_FEATURES); - - return new Promise((resolve) => { - const handleAuthMessage = (event: MessageEvent) => { - // Note that though the browser block opening window and popup a tip, - // the user can be still open it from the tip. In this case, the `opener` - // is null, and we should still process the authorizing message - const isValidOpener = !!(opener && event.source === opener); - const isValidOrigin = event.origin === location.origin; - const isValidResponse = event.data ? event.data.type === 'authorizing' : false; - const isValidState = event.data ? event.data.state === STATE : false; - if (!isValidOpener || !isValidOrigin || !isValidResponse || !isValidState) { - return; - } - window.removeEventListener('message', handleAuthMessage); - resolve(event.data?.payload); - }; - - window.addEventListener('message', handleAuthMessage); - // if there isn't any message from opener window in 300s, remove the message handler - timeout(300 * 1000).then(() => { - window.removeEventListener('message', handleAuthMessage); - resolve({ error: 'authorizing_timeout', error_description: 'Authorizing timeout' }); - }); - }); + return waitForOAuthResult(STATE, opener); }; diff --git a/src/gitlab-auth.ts b/src/gitlab-auth.ts index 661579031..e612e36c2 100644 --- a/src/gitlab-auth.ts +++ b/src/gitlab-auth.ts @@ -3,7 +3,7 @@ * @author netcon */ -import { timeout, createOAuthState } from './github-auth'; +import { createOAuthState, waitForOAuthResult } from './oauth-web'; const GITLAB_ORIGIN = 'https://gitlab.com'; const OAUTH_REDIRECT_URI = `${location.origin}/api/gitlab-auth-callback`; @@ -25,28 +25,5 @@ const createAuthorizeUrl = (state: string) => { export const ConnectToGitLab = async () => { const STATE = createOAuthState(); const opener = window.open(createAuthorizeUrl(STATE), '_blank', OPEN_WINDOW_FEATURES); - - return new Promise((resolve) => { - const handleAuthMessage = (event: MessageEvent) => { - // Note that though the browser block opening window and popup a tip, - // the user can be still open it from the tip. In this case, the `opener` - // is null, and we should still process the authorizing message - const isValidOpener = !!(opener && event.source === opener); - const isValidOrigin = event.origin === location.origin; - const isValidResponse = event.data ? event.data.type === 'authorizing' : false; - const isValidState = event.data ? event.data.state === STATE : false; - if (!isValidOpener || !isValidOrigin || !isValidResponse || !isValidState) { - return; - } - window.removeEventListener('message', handleAuthMessage); - resolve(event.data?.payload); - }; - - window.addEventListener('message', handleAuthMessage); - // if there isn't any message from opener window in 300s, remove the message handler - timeout(300 * 1000).then(() => { - window.removeEventListener('message', handleAuthMessage); - resolve({ error: 'authorizing_timeout', error_description: 'Authorizing timeout' }); - }); - }); + return waitForOAuthResult(STATE, opener); }; diff --git a/src/oauth-callback.ts b/src/oauth-callback.ts new file mode 100644 index 000000000..0cc3db979 --- /dev/null +++ b/src/oauth-callback.ts @@ -0,0 +1,58 @@ +import { getOAuthBroadcastChannelName } from './oauth-common'; + +export const MISSING_CODE_ERROR = { + error: 'request_invalid', + error_description: 'Missing code', +}; +export const INVALID_ORIGIN_ERROR = { + error: 'request_invalid', + error_description: 'Invalid origin', +}; +export const UNKNOWN_ERROR = { + error: 'internal_error', + error_description: 'Unknown error', +}; + +const createResponseHtml = (title: string, text: string, script: string) => ` + + + + ${title} + + +

${text}

+ + + +`; + +export const createAuthorizeResultHtml = ( + title: string, + data: Record, + state: string, + origin: string, +) => { + const sanitizedState = state.replace(/[^a-zA-Z0-9]/g, ''); + const result = { + type: 'authorizing', + payload: data, + state: sanitizedState, + }; + const resultStr = JSON.stringify(result).replace(/ `${OAUTH_BROADCAST_CHANNEL_PREFIX}${state}`; diff --git a/src/oauth-web.ts b/src/oauth-web.ts new file mode 100644 index 000000000..c00588e04 --- /dev/null +++ b/src/oauth-web.ts @@ -0,0 +1,76 @@ +import { getOAuthBroadcastChannelName } from './oauth-common'; + +export { getOAuthBroadcastChannelName } from './oauth-common'; + +interface OAuthResultMessage { + type: 'authorizing'; + payload: Record; + state: string; +} + +export const createOAuthState = () => { + const bytes = new Uint8Array(16); + window.crypto.getRandomValues(bytes); + return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(''); +}; + +const isOAuthResultMessage = (data: unknown, state: string): data is OAuthResultMessage => { + if (!data || typeof data !== 'object') { + return false; + } + const message = data as Partial; + return ( + message.type === 'authorizing' && + message.state === state && + message.payload !== null && + typeof message.payload === 'object' + ); +}; + +export const waitForOAuthResult = ( + state: string, + opener: Window | null, + timeoutMs = 300 * 1000, +): Promise> => { + const channel = + typeof BroadcastChannel === 'undefined' ? undefined : new BroadcastChannel(getOAuthBroadcastChannelName(state)); + + return new Promise((resolve) => { + let settled = false; + + const cleanup = () => { + window.removeEventListener('message', handleWindowMessage); + channel?.removeEventListener('message', handleBroadcastMessage); + channel?.close(); + window.clearTimeout(timeoutId); + }; + + const finish = (data: unknown) => { + if (settled || !isOAuthResultMessage(data, state)) { + return; + } + settled = true; + cleanup(); + resolve(data.payload); + }; + + const handleBroadcastMessage = (event: MessageEvent) => finish(event.data); + const handleWindowMessage = (event: MessageEvent) => { + if (!opener || event.source !== opener || event.origin !== location.origin) { + return; + } + finish(event.data); + }; + + channel?.addEventListener('message', handleBroadcastMessage); + window.addEventListener('message', handleWindowMessage); + const timeoutId = window.setTimeout(() => { + if (settled) { + return; + } + settled = true; + cleanup(); + resolve({ error: 'authorizing_timeout', error_description: 'Authorizing timeout' }); + }, timeoutMs); + }); +}; diff --git a/webpack.config.js b/webpack.config.js index 1df4501a5..e5704108c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -65,6 +65,7 @@ export default (env, argv) => { plugins: [ new CopyPlugin({ patterns: [ + { from: 'public/_headers', to: '_headers', toType: 'file' }, { from: 'public/favicon*', to: '[name][ext]' }, { from: 'public/manifest.json', to: '[name][ext]' }, { from: 'public/robots.txt', to: '[name][ext]' }, From ee48e38c6b6b3528f29eb960f4683a5b5eddb56f Mon Sep 17 00:00:00 2001 From: netcon Date: Tue, 11 Aug 2026 14:52:43 +0800 Subject: [PATCH 4/9] chore: bump vscode to 1.132.0 (#712) --- package-lock.json | 8 +- package.json | 2 +- vscode-web/.VERSION | 2 +- vscode-web/package-lock.json | 4 +- vscode-web/package.json | 2 +- vscode-web/scripts/.patch | 3 +- vscode-web/src/setup.d.ts | 1 - .../parts/activitybar/activitybarPart.ts | 4 +- .../files/browser/editors/fileEditorInput.ts | 487 ------------------ .../services/label/common/labelService.ts | 14 +- 10 files changed, 24 insertions(+), 503 deletions(-) delete mode 100644 vscode-web/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts diff --git a/package-lock.json b/package-lock.json index d77cffa34..2db196954 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "ISC", "devDependencies": { "@cloudflare/workers-types": "^4.20250109.0", - "@github1s/vscode-web": "^0.28.1", + "@github1s/vscode-web": "^0.29.0", "chokidar": "^4.0.3", "clean-css": "^5.3.3", "copy-webpack-plugin": "^14.0.0", @@ -251,9 +251,9 @@ } }, "node_modules/@github1s/vscode-web": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@github1s/vscode-web/-/vscode-web-0.28.1.tgz", - "integrity": "sha512-dJ2mXQ+/xdBrOcLuqYBKTjmA04Rr8Fg2au+Xu5mL1JgsE+FWe5AA0STgPTvuy8Zu6zdzlRJ7z9LOLzTh8DI6Dw==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@github1s/vscode-web/-/vscode-web-0.29.0.tgz", + "integrity": "sha512-e0d+8B8F1rdDjoCSCw8F08R22MEXpmnQ5iN8H/d/WzUDDf6igiMWOnVhKOlF3IO0rbMHs5cZEsSCbkRv08v2OA==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index b5a1e6605..bf9256fc6 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "license": "ISC", "devDependencies": { "@cloudflare/workers-types": "^4.20250109.0", - "@github1s/vscode-web": "^0.28.1", + "@github1s/vscode-web": "^0.29.0", "chokidar": "^4.0.3", "clean-css": "^5.3.3", "copy-webpack-plugin": "^14.0.0", diff --git a/vscode-web/.VERSION b/vscode-web/.VERSION index 21f81fe80..12f5b64c8 100644 --- a/vscode-web/.VERSION +++ b/vscode-web/.VERSION @@ -1 +1 @@ -1.131.0 \ No newline at end of file +1.132.0 \ No newline at end of file diff --git a/vscode-web/package-lock.json b/vscode-web/package-lock.json index ea17e2ff8..da933de2c 100644 --- a/vscode-web/package-lock.json +++ b/vscode-web/package-lock.json @@ -1,12 +1,12 @@ { "name": "@github1s/vscode-web", - "version": "0.28.1", + "version": "0.29.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@github1s/vscode-web", - "version": "0.28.1", + "version": "0.29.0", "license": "MIT", "devDependencies": { "chokidar": "^4.0.3", diff --git a/vscode-web/package.json b/vscode-web/package.json index 84004815b..970031796 100644 --- a/vscode-web/package.json +++ b/vscode-web/package.json @@ -1,6 +1,6 @@ { "name": "@github1s/vscode-web", - "version": "0.28.1", + "version": "0.29.0", "description": "VS Code web for GitHub1s", "author": "github1s", "license": "MIT", diff --git a/vscode-web/scripts/.patch b/vscode-web/scripts/.patch index 5a85a9d63..6771478ab 100644 --- a/vscode-web/scripts/.patch +++ b/vscode-web/scripts/.patch @@ -1,8 +1,7 @@ { - "src/vs/workbench/browser/parts/activitybar/activitybarPart.ts": "1c5075a0d54829eadbb19a214f9913558ddab03c3e088b2ec3590b49b2b8c460", + "src/vs/workbench/browser/parts/activitybar/activitybarPart.ts": "b1608b71d4ff7392a42d80a8a406a97913717bb6af7be703c90f73c3f4520253", "src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css": "8ffe6921a1c36709db7ed7f5eb705782f55e90acdc780c7722030b0f9e6d6fc5", "src/vs/workbench/browser/web.main.ts": "180c4439bcb518402e34c9b8b154bcd204b4b2e47a3edf2b1c77c612a8ff2bf5", - "src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts": "e986095a30dbea54af56c25fa1b184b55f34f8914129b27e2f20d8c4ea9fd16b", "src/vs/workbench/contrib/webview/browser/pre/index.html": "9f4a216e3c7abfa81d3afa9e215a36e935eb92b9193ae244cf80b1904d3fae0c", "src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts": "16fc1f8830432097a2de87ba04f9f11e930408df8f672bb7a4bbbe3c1a7c509d", "src/vs/workbench/services/label/common/labelService.ts": "9e4e2aafeabc04ccf183b8f2f4ce8a4d39f240831e4005b06149684458caa8d4", diff --git a/vscode-web/src/setup.d.ts b/vscode-web/src/setup.d.ts index 821362da9..c20ff11f8 100644 --- a/vscode-web/src/setup.d.ts +++ b/vscode-web/src/setup.d.ts @@ -4,7 +4,6 @@ declare var _VSCODE_WEB: { workspaceId?: string; // the identifier to distinguish workspace workspaceLabel?: string; // the label shown on explorer hideTextFileLabelDecorations?: boolean; // whether hide the readonly icon for readonly files - allowEditorLabelOverride?: boolean; // whether allow override editor label // custom builtin extensions, types see IBundledExtension[] builtinExtensions?: any[] | ((builtinExtensions: any[]) => any[]); logo?: { diff --git a/vscode-web/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/vscode-web/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index e349efa8e..e9cf047c5 100644 --- a/vscode-web/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/vscode-web/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -134,6 +134,8 @@ export class ActivitybarPart extends Part { private updateCompactStyle(): void { if (this.element) { this.element.classList.toggle('compact', this._isCompact); + // Mirrored on the workbench root for floatingPanels.css + this.layoutService.mainContainer.classList.toggle('activitybar-compact', this._isCompact); this.element.style.setProperty('--activity-bar-width', `${this.baseWidth}px`); this.element.style.setProperty('--activity-bar-action-height', `${this.actionHeight}px`); this.element.style.setProperty('--activity-bar-icon-size', `${this._isCompact ? ActivitybarPart.COMPACT_ICON_SIZE : ActivitybarPart.ICON_SIZE}px`); @@ -394,7 +396,7 @@ export class ActivityBarCompositeBar extends PaneCompositeBar { /* above codes are changed by github1s */ // Menubar: install a custom menu bar depending on configuration - this.menuBar.value = this._register(this.instantiationService.createInstance(CustomMenubarControl)); + this.menuBar.value = this.instantiationService.createInstance(CustomMenubarControl); this.menuBar.value.create(this.menuBarContainer); } diff --git a/vscode-web/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts b/vscode-web/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts deleted file mode 100644 index 8ccc31234..000000000 --- a/vscode-web/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts +++ /dev/null @@ -1,487 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI } from '../../../../../base/common/uri.js'; -import { IFileEditorInput, Verbosity, GroupIdentifier, IMoveResult, EditorInputCapabilities, IEditorDescriptor, IEditorPane, IUntypedEditorInput, DEFAULT_EDITOR_ASSOCIATION, IUntypedFileEditorInput, findViewStateForEditor, isResourceEditorInput, IFileEditorInputOptions } from '../../../../common/editor.js'; -import { EditorInput, IUntypedEditorOptions } from '../../../../common/editor/editorInput.js'; -import { AbstractTextResourceEditorInput } from '../../../../common/editor/textResourceEditorInput.js'; -import { ITextResourceEditorInput } from '../../../../../platform/editor/common/editor.js'; -import { BinaryEditorModel } from '../../../../common/editor/binaryEditorModel.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; -import { ITextFileService, TextFileEditorModelState, TextFileResolveReason, TextFileOperationError, TextFileOperationResult, ITextFileEditorModel, EncodingMode } from '../../../../services/textfile/common/textfiles.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IReference, dispose, DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { FILE_EDITOR_INPUT_ID, TEXT_FILE_EDITOR_ID, BINARY_FILE_EDITOR_ID } from '../../common/files.js'; -import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { isEqual } from '../../../../../base/common/resources.js'; -import { Event } from '../../../../../base/common/event.js'; -import { Schemas } from '../../../../../base/common/network.js'; -import { createTextBufferFactory } from '../../../../../editor/common/model/textModel.js'; -import { IPathService } from '../../../../services/path/common/pathService.js'; -import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js'; -import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; -import { ICustomEditorLabelService } from '../../../../services/editor/common/customEditorLabelService.js'; - -const enum ForceOpenAs { - None, - Text, - Binary -} - -/** - * A file editor input is the input type for the file editor of file system resources. - */ -export class FileEditorInput extends AbstractTextResourceEditorInput implements IFileEditorInput { - - override get typeId(): string { - return FILE_EDITOR_INPUT_ID; - } - - override get editorId(): string | undefined { - return DEFAULT_EDITOR_ASSOCIATION.id; - } - - override get capabilities(): EditorInputCapabilities { - let capabilities = EditorInputCapabilities.CanSplitInGroup; - - if (this.model) { - if (this.model.isReadonly()) { - capabilities |= EditorInputCapabilities.Readonly; - } - } else { - if (this.fileService.hasProvider(this.resource)) { - if (this.filesConfigurationService.isReadonly(this.resource)) { - capabilities |= EditorInputCapabilities.Readonly; - } - } else { - capabilities |= EditorInputCapabilities.Untitled; - } - } - - if (!(capabilities & EditorInputCapabilities.Readonly)) { - capabilities |= EditorInputCapabilities.CanDropIntoEditor; - } - - return capabilities; - } - - private preferredName: string | undefined; - private preferredDescription: string | undefined; - private preferredEncoding: string | undefined; - private preferredLanguageId: string | undefined; - private preferredContents: string | undefined; - - private forceOpenAs: ForceOpenAs = ForceOpenAs.None; - - private model: ITextFileEditorModel | undefined = undefined; - private cachedTextFileModelReference: IReference | undefined = undefined; - - private readonly modelListeners = this._register(new DisposableStore()); - - constructor( - resource: URI, - preferredResource: URI | undefined, - preferredName: string | undefined, - preferredDescription: string | undefined, - preferredEncoding: string | undefined, - preferredLanguageId: string | undefined, - preferredContents: string | undefined, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITextFileService textFileService: ITextFileService, - @ITextModelService private readonly textModelService: ITextModelService, - @ILabelService labelService: ILabelService, - @IFileService fileService: IFileService, - @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, - @IEditorService editorService: IEditorService, - @IPathService private readonly pathService: IPathService, - @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, - @ICustomEditorLabelService customEditorLabelService: ICustomEditorLabelService - ) { - super(resource, preferredResource, editorService, textFileService, labelService, fileService, filesConfigurationService, textResourceConfigurationService, customEditorLabelService); - - this.model = this.textFileService.files.get(resource); - - if (preferredName) { - this.setPreferredName(preferredName); - } - - if (preferredDescription) { - this.setPreferredDescription(preferredDescription); - } - - if (preferredEncoding) { - this.setPreferredEncoding(preferredEncoding); - } - - if (preferredLanguageId) { - this.setPreferredLanguageId(preferredLanguageId); - } - - if (typeof preferredContents === 'string') { - this.setPreferredContents(preferredContents); - } - - // Attach to model that matches our resource once created - this._register(this.textFileService.files.onDidCreate(model => this.onDidCreateTextFileModel(model))); - - // If a file model already exists, make sure to wire it in - if (this.model) { - this.registerModelListeners(this.model); - } - } - - private onDidCreateTextFileModel(model: ITextFileEditorModel): void { - - // Once the text file model is created, we keep it inside - // the input to be able to implement some methods properly - if (isEqual(model.resource, this.resource)) { - this.model = model; - - this.registerModelListeners(model); - } - } - - private registerModelListeners(model: ITextFileEditorModel): void { - - // Clear any old - this.modelListeners.clear(); - - // re-emit some events from the model - this.modelListeners.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - this.modelListeners.add(model.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); - - // important: treat save errors as potential dirty change because - // a file that is in save conflict or error will report dirty even - // if auto save is turned on. - this.modelListeners.add(model.onDidSaveError(() => this._onDidChangeDirty.fire())); - - // remove model association once it gets disposed - this.modelListeners.add(Event.once(model.onWillDispose)(() => { - this.modelListeners.clear(); - this.model = undefined; - })); - } - - override getName(): string { - return this.preferredName || super.getName(); - } - - setPreferredName(name: string): void { - if (!this.allowLabelOverride()) { - return; // block for specific schemes we consider to be owning - } - - if (this.preferredName !== name) { - this.preferredName = name; - - this._onDidChangeLabel.fire(); - } - } - - private allowLabelOverride(): boolean { - /* below codes are changed by github1s */ - if (globalThis._VSCODE_WEB?.allowEditorLabelOverride) return true; - /* above codes are changed by github1s */ - return this.resource.scheme !== this.pathService.defaultUriScheme && - this.resource.scheme !== Schemas.vscodeUserData && - this.resource.scheme !== Schemas.file && - this.resource.scheme !== Schemas.vscodeRemote; - } - - getPreferredName(): string | undefined { - return this.preferredName; - } - - override isReadonly(): boolean | IMarkdownString { - return this.model ? this.model.isReadonly() : this.filesConfigurationService.isReadonly(this.resource); - } - - override getDescription(verbosity?: Verbosity): string | undefined { - return this.preferredDescription || super.getDescription(verbosity); - } - - setPreferredDescription(description: string): void { - if (!this.allowLabelOverride()) { - return; // block for specific schemes we consider to be owning - } - - if (this.preferredDescription !== description) { - this.preferredDescription = description; - - this._onDidChangeLabel.fire(); - } - } - - getPreferredDescription(): string | undefined { - return this.preferredDescription; - } - - override getTitle(verbosity?: Verbosity): string { - let title = super.getTitle(verbosity); - - const preferredTitle = this.getPreferredTitle(); - if (preferredTitle) { - title = `${preferredTitle} (${title})`; - } - - return title; - } - - protected getPreferredTitle(): string | undefined { - if (this.preferredName && this.preferredDescription) { - return `${this.preferredName} ${this.preferredDescription}`; - } - - if (this.preferredName || this.preferredDescription) { - return this.preferredName ?? this.preferredDescription; - } - - return undefined; - } - - getEncoding(): string | undefined { - if (this.model) { - return this.model.getEncoding(); - } - - return this.preferredEncoding; - } - - getPreferredEncoding(): string | undefined { - return this.preferredEncoding; - } - - async setEncoding(encoding: string, mode: EncodingMode): Promise { - this.setPreferredEncoding(encoding); - - return this.model?.setEncoding(encoding, mode); - } - - setPreferredEncoding(encoding: string): void { - this.preferredEncoding = encoding; - - // encoding is a good hint to open the file as text - this.setForceOpenAsText(); - } - - getLanguageId(): string | undefined { - if (this.model) { - return this.model.getLanguageId(); - } - - return this.preferredLanguageId; - } - - getPreferredLanguageId(): string | undefined { - return this.preferredLanguageId; - } - - setLanguageId(languageId: string, source?: string): void { - this.setPreferredLanguageId(languageId); - - this.model?.setLanguageId(languageId, source); - } - - setPreferredLanguageId(languageId: string): void { - this.preferredLanguageId = languageId; - - // languages are a good hint to open the file as text - this.setForceOpenAsText(); - } - - setPreferredContents(contents: string): void { - this.preferredContents = contents; - - // contents is a good hint to open the file as text - this.setForceOpenAsText(); - } - - setForceOpenAsText(): void { - this.forceOpenAs = ForceOpenAs.Text; - } - - setForceOpenAsBinary(): void { - this.forceOpenAs = ForceOpenAs.Binary; - } - - override isDirty(): boolean { - return !!(this.model?.isDirty()); - } - - override isSaving(): boolean { - if (this.model?.hasState(TextFileEditorModelState.SAVED) || this.model?.hasState(TextFileEditorModelState.CONFLICT) || this.model?.hasState(TextFileEditorModelState.ERROR)) { - return false; // require the model to be dirty and not in conflict or error state - } - - // Note: currently not checking for ModelState.PENDING_SAVE for a reason - // because we currently miss an event for this state change on editors - // and it could result in bad UX where an editor can be closed even though - // it shows up as dirty and has not finished saving yet. - - if (this.filesConfigurationService.hasShortAutoSaveDelay(this)) { - return true; // a short auto save is configured, treat this as being saved - } - - return super.isSaving(); - } - - override prefersEditorPane>(editorPanes: T[]): T | undefined { - if (this.forceOpenAs === ForceOpenAs.Binary) { - return editorPanes.find(editorPane => editorPane.typeId === BINARY_FILE_EDITOR_ID); - } - - return editorPanes.find(editorPane => editorPane.typeId === TEXT_FILE_EDITOR_ID); - } - - override resolve(options?: IFileEditorInputOptions): Promise { - - // Resolve as binary - if (this.forceOpenAs === ForceOpenAs.Binary) { - return this.doResolveAsBinary(); - } - - // Resolve as text - return this.doResolveAsText(options); - } - - private async doResolveAsText(options?: IFileEditorInputOptions): Promise { - try { - - // Unset preferred contents after having applied it once - // to prevent this property to stick. We still want future - // `resolve` calls to fetch the contents from disk. - const preferredContents = this.preferredContents; - this.preferredContents = undefined; - - // Resolve resource via text file service and only allow - // to open binary files if we are instructed so - await this.textFileService.files.resolve(this.resource, { - languageId: this.preferredLanguageId, - encoding: this.preferredEncoding, - contents: typeof preferredContents === 'string' ? createTextBufferFactory(preferredContents) : undefined, - reload: { async: true }, // trigger a reload of the model if it exists already but do not wait to show the model - allowBinary: this.forceOpenAs === ForceOpenAs.Text, - reason: TextFileResolveReason.EDITOR, - limits: this.ensureLimits(options) - }); - - // This is a bit ugly, because we first resolve the model and then resolve a model reference. the reason being that binary - // or very large files do not resolve to a text file model but should be opened as binary files without text. First calling into - // resolve() ensures we are not creating model references for these kind of resources. - // In addition we have a bit of payload to take into account (encoding, reload) that the text resolver does not handle yet. - if (!this.cachedTextFileModelReference) { - this.cachedTextFileModelReference = await this.textModelService.createModelReference(this.resource) as IReference; - } - - const model = this.cachedTextFileModelReference.object; - - // It is possible that this input was disposed before the model - // finished resolving. As such, we need to make sure to dispose - // the model reference to not leak it. - if (this.isDisposed()) { - this.disposeModelReference(); - } - - return model; - } catch (error) { - - // Handle binary files with binary model - if ((error).textFileOperationResult === TextFileOperationResult.FILE_IS_BINARY) { - return this.doResolveAsBinary(); - } - - // Bubble any other error up - throw error; - } - } - - private async doResolveAsBinary(): Promise { - const model = this.instantiationService.createInstance(BinaryEditorModel, this.preferredResource, this.getName()); - await model.resolve(); - - return model; - } - - isResolved(): boolean { - return !!this.model; - } - - override async rename(group: GroupIdentifier, target: URI): Promise { - return { - editor: { - resource: target, - encoding: this.getEncoding(), - options: { - viewState: findViewStateForEditor(this, group, this.editorService) - } - } - }; - } - - override toUntyped(options?: IUntypedEditorOptions): ITextResourceEditorInput { - const untypedInput: IUntypedFileEditorInput = { - resource: this.preferredResource, - forceFile: true, - options: { - override: this.editorId - } - }; - - if (typeof options?.preserveViewState === 'number') { - untypedInput.encoding = this.getEncoding(); - untypedInput.languageId = this.getLanguageId(); - untypedInput.contents = (() => { - const model = this.textFileService.files.get(this.resource); - if (model?.isDirty() && !model.textEditorModel.isTooLargeForHeapOperation()) { - return model.textEditorModel.getValue(); // only if dirty and not too large - } - - return undefined; - })(); - - untypedInput.options = { - ...untypedInput.options, - viewState: findViewStateForEditor(this, options.preserveViewState, this.editorService) - }; - } - - return untypedInput; - } - - override matches(otherInput: EditorInput | IUntypedEditorInput): boolean { - if (this === otherInput) { - return true; - } - - if (otherInput instanceof FileEditorInput) { - return isEqual(otherInput.resource, this.resource); - } - - if (isResourceEditorInput(otherInput)) { - return super.matches(otherInput); - } - - return false; - } - - override dispose(): void { - - // Model - this.model = undefined; - - // Model reference - this.disposeModelReference(); - - super.dispose(); - } - - private disposeModelReference(): void { - dispose(this.cachedTextFileModelReference); - this.cachedTextFileModelReference = undefined; - } -} diff --git a/vscode-web/src/vs/workbench/services/label/common/labelService.ts b/vscode-web/src/vs/workbench/services/label/common/labelService.ts index 2557d4b4f..7832cb933 100644 --- a/vscode-web/src/vs/workbench/services/label/common/labelService.ts +++ b/vscode-web/src/vs/workbench/services/label/common/labelService.ts @@ -77,7 +77,9 @@ const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoin const posixPathSeparatorRegexp = /\//g; // on Unix, backslash is a valid filename character const winPathSeparatorRegexp = /[\\\/]/g; // on Windows, neither slash nor backslash are valid filename characters -const labelMatchingRegexp = /\$\{(scheme|authoritySuffix|authority|path|(query)\.(.+?))\}/g; +// below codes are changed by github1s +const labelMatchingRegexp = /\$\{(scheme|authoritySuffix(?::\d+)?|authority|path|(query)\.(.+?))\}/g; +// above codes are changed by github1s function hasDriveLetterIgnorePlatform(path: string): boolean { return !!(path && path[2] === ':'); @@ -452,13 +454,19 @@ export class LabelService extends Disposable implements ILabelService { } private formatUri(resource: URI, formatting: ResourceLabelFormatting, forceNoTildify?: boolean): string { - let label = formatting.label.replace(labelMatchingRegexp, (match, token, qsToken, qsValue) => { + // below codes are changed by github1s + let label = formatting.label.replace(labelMatchingRegexp, (match, tokenWithArgument, qsToken, qsValue) => { + const [token, argument] = tokenWithArgument.split(':'); + // above codes are changed by github1s switch (token) { case 'scheme': return resource.scheme; case 'authority': return resource.authority; case 'authoritySuffix': { const i = resource.authority.indexOf('+'); - return i === -1 ? resource.authority : resource.authority.slice(i + 1); + // below codes are changed by github1s + const authoritySuffix = i === -1 ? resource.authority : resource.authority.slice(i + 1); + return argument === undefined ? authoritySuffix : authoritySuffix.slice(0, Number(argument)); + // above codes are changed by github1s } case 'path': { let pathValue = resource.path; From 1e9c09bb50a0845737b5c6755fa143563e4ea9b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 12:43:12 +0530 Subject: [PATCH 5/9] chore(deps-dev): bump js-yaml from 4.3.0 to 4.3.1 (#713) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.0 to 4.3.1. - [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.3.0...4.3.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.3.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2db196954..e08421bc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5239,9 +5239,9 @@ } }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { From b36aff808d555be9059ab2dac6cb26f2ab32d8e4 Mon Sep 17 00:00:00 2001 From: netcon Date: Tue, 11 Aug 2026 17:54:33 +0800 Subject: [PATCH 6/9] feat: use resourceLabelFormatters (#714) * feat: use resourceLabelFormatters * feat: use absolute paths uniformly * feat: fit uri state * feat: standardize uri authorization * feat: avoid unnecessary readfile requests --- extensions/github1s/package.json | 29 +++++- .../src/adapters/bitbucket1s/parse-path.ts | 11 ++- .../src/adapters/bitbucket1s/router-parser.ts | 4 +- .../src/adapters/github1s/data-source.ts | 34 ++++--- .../src/adapters/github1s/parse-path.ts | 2 +- .../src/adapters/github1s/router-parser.ts | 4 +- .../src/adapters/gitlab1s/data-source.ts | 33 ++++--- .../src/adapters/gitlab1s/parse-path.ts | 2 +- .../src/adapters/gitlab1s/router-parser.ts | 4 +- .../src/adapters/npmjs1s/data-source.ts | 12 +-- .../src/adapters/npmjs1s/router-parser.ts | 4 +- .../src/adapters/ossinsight/data-source.ts | 4 +- .../src/adapters/ossinsight/router-parser.ts | 3 +- .../src/adapters/sourcegraph/data-source.ts | 57 ++++++----- .../github1s/src/adapters/sourcegraph/file.ts | 6 +- extensions/github1s/src/adapters/types.ts | 5 +- extensions/github1s/src/changes/files.ts | 51 ++++------ extensions/github1s/src/changes/index.ts | 6 +- extensions/github1s/src/changes/quick-diff.ts | 53 +++++----- extensions/github1s/src/commands/blame.ts | 10 +- .../github1s/src/commands/code-review.ts | 10 +- extensions/github1s/src/commands/commit.ts | 20 ++-- extensions/github1s/src/commands/editor.ts | 51 ++++------ extensions/github1s/src/commands/global.ts | 6 +- extensions/github1s/src/commands/ref.ts | 4 +- extensions/github1s/src/extension.ts | 17 +--- extensions/github1s/src/helpers/submodule.ts | 18 ++-- extensions/github1s/src/helpers/util.ts | 16 +-- .../github1s/src/listeners/router/explorer.ts | 2 +- extensions/github1s/src/listeners/vscode.ts | 14 +-- extensions/github1s/src/messages.ts | 2 +- .../src/providers/decorations/changed-file.ts | 27 +++-- .../providers/decorations/source-control.ts | 14 ++- .../src/providers/decorations/submodule.ts | 4 + .../github1s/src/providers/definition.ts | 29 +++--- .../github1s/src/providers/file-search.ts | 35 ++++--- .../src/providers/file-system/index.ts | 98 +++++++------------ extensions/github1s/src/providers/hover.ts | 25 ++--- extensions/github1s/src/providers/index.ts | 5 +- .../github1s/src/providers/reference.ts | 18 ++-- .../github1s/src/providers/text-search.ts | 9 +- .../github1s/src/repository/commit-manager.ts | 4 +- extensions/github1s/src/repository/index.ts | 26 +++-- extensions/github1s/src/router/index.ts | 74 +++++++++----- extensions/github1s/src/statusbar/checkout.ts | 2 +- extensions/github1s/src/statusbar/sponsors.ts | 2 +- .../github1s/src/views/code-review-list.ts | 15 ++- extensions/github1s/src/views/commit-list.ts | 19 ++-- src/index.ts | 1 - src/oauth-web.ts | 2 - 50 files changed, 448 insertions(+), 455 deletions(-) diff --git a/extensions/github1s/package.json b/extensions/github1s/package.json index cafb1e48c..927bdf563 100644 --- a/extensions/github1s/package.json +++ b/extensions/github1s/package.json @@ -26,6 +26,32 @@ "vscode": "^1.48.0" }, "contributes": { + "resourceLabelFormatters": [ + { + "scheme": "github1s", + "authority": "**/*+?*", + "formatting": { + "label": "${path} (${authoritySuffix:7})", + "separator": "/" + } + }, + { + "scheme": "gitlab1s", + "authority": "**/*+?*", + "formatting": { + "label": "${path} (${authoritySuffix:7})", + "separator": "/" + } + }, + { + "scheme": "bitbucket1s", + "authority": "**/*+?*", + "formatting": { + "label": "${path} (${authoritySuffix:7})", + "separator": "/" + } + } + ], "viewsContainers": { "activitybar": [ { @@ -596,7 +622,8 @@ "scripts": { "clean": "rm -rf dist out", "watch": "webpack --config webpack.config.js --watch", - "compile": "webpack --config webpack.config.js --mode production" + "compile": "webpack --config webpack.config.js --mode production", + "test": "node --experimental-strip-types --test test/*.test.ts" }, "keywords": [], "author": "", diff --git a/extensions/github1s/src/adapters/bitbucket1s/parse-path.ts b/extensions/github1s/src/adapters/bitbucket1s/parse-path.ts index 2ac025f6d..4ba8f48f1 100644 --- a/extensions/github1s/src/adapters/bitbucket1s/parse-path.ts +++ b/extensions/github1s/src/adapters/bitbucket1s/parse-path.ts @@ -13,9 +13,14 @@ const parseTreeOrBlobUrl = async (path: string): Promise => { const repoFullName = `${owner}/${repo}`; const dataSource = SourcegraphDataSource.getInstance('bitbucket'); const { ref, path: filePath } = await dataSource.extractRefPath(repoFullName, restParts.join('/')); - const fileType = await dataSource.detectPathFileType(repo, ref, filePath); + const fileType = await dataSource.detectPathFileType(repoFullName, ref, filePath); - return { pageType: fileType === FileType.Directory ? PageType.Tree : PageType.Blob, repo, ref, filePath }; + return { + pageType: fileType === FileType.Directory ? PageType.Tree : PageType.Blob, + repo: repoFullName, + ref, + filePath, + }; }; const parseCommitsUrl = async (path: string): Promise => { @@ -64,6 +69,6 @@ export const parseBitbucketPath = async (path: string): Promise => repo: 'atlassian/clover', ref: 'HEAD', pageType: PageType.Tree, - filePath: '', + filePath: '/', }; }; diff --git a/extensions/github1s/src/adapters/bitbucket1s/router-parser.ts b/extensions/github1s/src/adapters/bitbucket1s/router-parser.ts index 658a7cd9a..d5d4de6a0 100644 --- a/extensions/github1s/src/adapters/bitbucket1s/router-parser.ts +++ b/extensions/github1s/src/adapters/bitbucket1s/router-parser.ts @@ -21,12 +21,12 @@ export class BitbucketRouterParser extends adapterTypes.RouterParser { } buildTreePath(repo: string, ref?: string, filePath?: string): string { - return ref ? (filePath ? `/${repo}/src/${ref}/${filePath}` : `/${repo}/src/${ref}`) : `/${repo}`; + return ref ? `/${repo}/src/${ref}${filePath && filePath !== '/' ? filePath : ''}` : `/${repo}`; } buildBlobPath(repo: string, ref: string, filePath: string, startLine?: number, endLine?: number): string { const hash = startLine ? (endLine ? `#L${startLine}-L${endLine}` : `#L${startLine}`) : ''; - return `/${repo}/src/${ref}/${filePath}${hash}`; + return `/${repo}/src/${ref}${filePath}${hash}`; } buildCommitListPath(repo: string): string { diff --git a/extensions/github1s/src/adapters/github1s/data-source.ts b/extensions/github1s/src/adapters/github1s/data-source.ts index 9734bd3d5..ba7497ce7 100644 --- a/extensions/github1s/src/adapters/github1s/data-source.ts +++ b/extensions/github1s/src/adapters/github1s/data-source.ts @@ -33,6 +33,7 @@ import { FILE_BLAME_QUERY } from './graphql'; import { GitHubFetcher } from './fetcher'; import { SourcegraphDataSource } from '../sourcegraph/data-source'; import { decorate, memorize } from '@/helpers/func'; +import { normalizePath, trimStart, concatPath, isString } from '@/helpers/util'; const parseRepoFullName = (repoFullName: string) => { const [owner, repo] = repoFullName.split('/'); @@ -41,7 +42,7 @@ const parseRepoFullName = (repoFullName: string) => { const encodeFilePath = (filePath: string): string => { const pathParts = filePath.split('/').filter(Boolean); - return pathParts.map((segment) => encodeURIComponent(segment)).join('/'); + return `/${pathParts.map((segment) => encodeURIComponent(segment)).join('/')}`; }; const FileTypeMap = { @@ -104,13 +105,13 @@ export class GitHub1sDataSource extends DataSource { @trySourcegraphApiFirst async provideDirectory(repoFullName: string, ref: string, path: string, recursive = false): Promise { const fetcher = GitHubFetcher.getInstance(); - const encodedPath = encodeFilePath(path); + const encodedPath = trimStart(encodeFilePath(path), '/'); // github api will return all files if `recursive` exists, even the value if false const recursiveParams = recursive ? { recursive } : {}; const requestParams = { ref, path: encodedPath, ...parseRepoFullName(repoFullName), ...recursiveParams }; const { data } = await fetcher.request('GET /repos/{owner}/{repo}/git/trees/{ref}:{path}', requestParams); const parseTreeItem = (treeItem): DirectoryEntry => ({ - path: treeItem.path, + path: concatPath(path, treeItem.path), type: FileTypeMap[treeItem.type] || FileType.File, commitSha: FileTypeMap[treeItem.type] === FileType.Submodule ? treeItem.sha || 'HEAD' : undefined, size: treeItem.size, @@ -126,7 +127,7 @@ export class GitHub1sDataSource extends DataSource { async provideFile(repoFullName: string, ref: string, path: string): Promise { const fetcher = GitHubFetcher.getInstance(); const { owner, repo } = parseRepoFullName(repoFullName); - const requestParams = { owner, repo, ref, path }; + const requestParams = { owner, repo, ref, path: trimStart(path, '/') }; const { data } = await fetcher.request('GET /repos/{owner}/{repo}/contents/{path}', requestParams); return { content: toUint8Array((data as any).content) }; } @@ -156,16 +157,16 @@ export class GitHub1sDataSource extends DataSource { const matchPathRef = (ref) => refAndPath.startsWith(`${ref}/`) || refAndPath === ref; const matchedRef = this.matchedRefsMap.get(repoFullName)?.find(matchPathRef); if (matchedRef) { - return { ref: matchedRef, path: refAndPath.slice(matchedRef.length + 1) }; + return { ref: matchedRef, path: normalizePath(refAndPath.slice(matchedRef.length + 1)) }; } const mapKey = `${repoFullName} ${refAndPath}`; if (!this.refPathPromiseMap.has(mapKey)) { const refPathPromise = new Promise<{ ref: string; path: string }>(async (resolve, reject) => { if (!refAndPath) { - return resolve({ ref: await this.getDefaultBranch(repoFullName), path: '' }); + return resolve({ ref: await this.getDefaultBranch(repoFullName), path: '/' }); } if (refAndPath.match(/^HEAD(\/.*)?$/i)) { - return resolve({ ref: 'HEAD', path: refAndPath.slice(5) }); + return resolve({ ref: 'HEAD', path: normalizePath(refAndPath.slice(5)) }); } const fetcher = GitHubFetcher.getInstance(); @@ -174,7 +175,8 @@ export class GitHub1sDataSource extends DataSource { const requestUrl = `GET /repos/{owner}/{repo}/git/extract-ref/{refAndPath}`; const response = await fetcher.request(requestUrl, requestParams).catch(reject); response?.data?.ref && this.matchedRefsMap.get(repoFullName)?.push(response.data.ref); - return resolve(response?.data || { ref: 'HEAD', path: '' }); + const result = response?.data || { ref: 'HEAD', path: '/' }; + return resolve({ ...result, path: normalizePath(result.path) }); }); this.refPathPromiseMap.set(mapKey, refPathPromise); } @@ -247,7 +249,7 @@ export class GitHub1sDataSource extends DataSource { page: options?.page, per_page: options?.pageSize, sha: options?.from, - path: options?.path, + path: isString(options?.path) ? trimStart(options.path, '/') : undefined, author: options?.author, }; const requestParams = { owner, repo, ...queryParams }; @@ -279,8 +281,8 @@ export class GitHub1sDataSource extends DataSource { createTime: data.commit.author?.date ? new Date(data.commit.author.date) : undefined, parents: data.parents.map((parent) => parent.sha) || [], files: data.files?.map((item) => ({ - path: item.filename || item.previous_filename!, - previousPath: item.previous_filename, + path: normalizePath(item.filename || item.previous_filename!), + previousPath: item.previous_filename ? normalizePath(item.previous_filename) : undefined, status: item.status as FileChangeStatus, })), avatarUrl: data.author?.avatar_url, @@ -299,8 +301,8 @@ export class GitHub1sDataSource extends DataSource { const { data } = await fetcher.request('GET /repos/{owner}/{repo}/commits/{ref}', requestParams); return ( data.files?.map((item) => ({ - path: item.filename || item.previous_filename!, - previousPath: item.previous_filename, + path: normalizePath(item.filename || item.previous_filename!), + previousPath: item.previous_filename ? normalizePath(item.previous_filename) : undefined, status: item.status as FileChangeStatus, })) || [] ); @@ -367,8 +369,8 @@ export class GitHub1sDataSource extends DataSource { const { data } = await fetcher.request('GET /repos/{owner}/{repo}/pulls/{pull_number}/files', filesRequestParams); return data.map((item) => ({ - path: item.filename, - previousPath: item.previous_filename, + path: normalizePath(item.filename), + previousPath: item.previous_filename ? normalizePath(item.previous_filename) : undefined, status: item.status as FileChangeStatus, })); } @@ -377,7 +379,7 @@ export class GitHub1sDataSource extends DataSource { async provideFileBlameRanges(repoFullName: string, ref: string, path: string): Promise { const fetcher = GitHubFetcher.getInstance(); const { owner, repo } = parseRepoFullName(repoFullName); - const requestParams = { owner, repo, ref, path }; + const requestParams = { owner, repo, ref, path: trimStart(path, '/') }; const data = await fetcher.graphql(FILE_BLAME_QUERY, requestParams); const blameRanges = (data as any)?.repository?.object?.blame?.ranges; diff --git a/extensions/github1s/src/adapters/github1s/parse-path.ts b/extensions/github1s/src/adapters/github1s/parse-path.ts index b16ab61a6..cf20392ef 100644 --- a/extensions/github1s/src/adapters/github1s/parse-path.ts +++ b/extensions/github1s/src/adapters/github1s/parse-path.ts @@ -164,6 +164,6 @@ export const parseGitHubPath = async (path: string): Promise => { repo: DEFAULT_REPO, ref: await getDefaultBranch(DEFAULT_REPO), pageType: PageType.Tree, - filePath: '', + filePath: '/', }; }; diff --git a/extensions/github1s/src/adapters/github1s/router-parser.ts b/extensions/github1s/src/adapters/github1s/router-parser.ts index 99345bb33..a38dab5bc 100644 --- a/extensions/github1s/src/adapters/github1s/router-parser.ts +++ b/extensions/github1s/src/adapters/github1s/router-parser.ts @@ -22,12 +22,12 @@ export class GitHub1sRouterParser extends adapterTypes.RouterParser { } buildTreePath(repo: string, ref?: string, filePath?: string): string { - return ref ? (filePath ? `/${repo}/tree/${ref}/${filePath}` : `/${repo}/tree/${ref}`) : `/${repo}`; + return ref ? `/${repo}/tree/${ref}${filePath && filePath !== '/' ? filePath : ''}` : `/${repo}`; } buildBlobPath(repo: string, ref: string, filePath: string, startLine?: number, endLine?: number): string { const hash = startLine ? (endLine ? `#L${startLine}-L${endLine}` : `#L${startLine}`) : ''; - return `/${repo}/blob/${ref}/${filePath}${hash}`; + return `/${repo}/blob/${ref}${filePath}${hash}`; } buildCommitListPath(repo: string): string { diff --git a/extensions/github1s/src/adapters/gitlab1s/data-source.ts b/extensions/github1s/src/adapters/gitlab1s/data-source.ts index d84c9940a..9b79a2a95 100644 --- a/extensions/github1s/src/adapters/gitlab1s/data-source.ts +++ b/extensions/github1s/src/adapters/gitlab1s/data-source.ts @@ -32,6 +32,7 @@ import { matchSorter } from 'match-sorter'; import { GitLabFetcher } from './fetcher'; import { SourcegraphDataSource } from '../sourcegraph/data-source'; import { decorate, memorize } from '@/helpers/func'; +import { trimStart, normalizePath, isString } from '@/helpers/util'; const FileTypeMap = { blob: FileType.File, @@ -102,13 +103,13 @@ export class GitLab1sDataSource extends DataSource { let page = 1; let files = []; const parseTreeItem = (treeItem): DirectoryEntry => ({ - path: treeItem.path.slice(path.length), + path: normalizePath(treeItem.path), type: FileTypeMap[treeItem.type] || FileType.File, - commitSha: FileTypeMap[treeItem.id] === FileType.Submodule ? treeItem.sha || 'HEAD' : undefined, + commitSha: FileTypeMap[treeItem.type] === FileType.Submodule ? treeItem.sha || 'HEAD' : undefined, size: treeItem.size, }); while (page > 0) { - const requestParams = { ref, page, path, repo, recursive }; + const requestParams = { ref, page, path: trimStart(path, '/'), repo, recursive }; const { data, headers } = await fetcher.request( 'GET /projects/{repo}/repository/tree?recursive={recursive}&per_page=100&page={page}&ref={ref}&path={path}', requestParams, @@ -127,7 +128,7 @@ export class GitLab1sDataSource extends DataSource { @trySourcegraphApiFirst async provideFile(repo: string, ref: string, path: string): Promise { const fetcher = GitLabFetcher.getInstance(); - const requestParams = { ref, path, repo }; + const requestParams = { ref, path: trimStart(path, '/'), repo }; const { data } = await fetcher.request('GET /projects/{repo}/repository/files/{path}?ref={ref}', requestParams); return { content: toUint8Array((data as any).content) }; } @@ -164,10 +165,10 @@ export class GitLab1sDataSource extends DataSource { @trySourcegraphApiFirst async extractRefPath(repo: string, refAndPath: string): Promise<{ ref: string; path: string }> { if (!refAndPath) { - return { ref: await this.getDefaultBranch(repo), path: '' }; + return { ref: await this.getDefaultBranch(repo), path: '/' }; } if (refAndPath.match(/^HEAD(\/.*)?$/i)) { - return { ref: 'HEAD', path: refAndPath.slice(5) }; + return { ref: 'HEAD', path: normalizePath(refAndPath.slice(5)) }; } if (!this.matchedRefsMap.has(repo)) { this.matchedRefsMap.set(repo, []); @@ -175,13 +176,13 @@ export class GitLab1sDataSource extends DataSource { const matchPathRef = (ref) => refAndPath.startsWith(`${ref}/`) || refAndPath === ref; const pathRef = this.matchedRefsMap.get(repo)?.find(matchPathRef); if (pathRef) { - return { ref: pathRef, path: refAndPath.slice(pathRef.length + 1) }; + return { ref: pathRef, path: normalizePath(refAndPath.slice(pathRef.length + 1)) }; } const [branches, tags] = await this.prepareAllRefs(repo); const exactRef = [...branches, ...tags].map((item) => item.name).find(matchPathRef); const ref = exactRef || refAndPath.split('/')[0] || 'HEAD'; exactRef && this.matchedRefsMap.get(repo)?.push(ref); - return { ref, path: refAndPath.slice(ref.length + 1) }; + return { ref, path: normalizePath(refAndPath.slice(ref.length + 1)) }; } async prepareAllRefs(repo: string) { @@ -250,7 +251,7 @@ export class GitLab1sDataSource extends DataSource { page: options?.page, per_page: options?.pageSize, sha: options?.from, - path: options?.path, + path: isString(options?.path) ? trimStart(options.path, '/') : undefined, author: options?.author, }; const requestParams = { repo, ...queryParams }; @@ -286,8 +287,8 @@ export class GitLab1sDataSource extends DataSource { createTime: data.created_at ? new Date(data.created_at) : undefined, parents: data.parent_ids || [], files: data.files?.map((item) => ({ - path: item.filename || item.previous_filename!, - previousPath: item.previous_filename, + path: normalizePath(item.filename || item.previous_filename!), + previousPath: item.previous_filename ? normalizePath(item.previous_filename) : undefined, status: item.status as FileChangeStatus, })), avatarUrl: data?.avatar_url, @@ -301,8 +302,8 @@ export class GitLab1sDataSource extends DataSource { const { data } = await fetcher.request('GET /projects/{repo}/repository/commits/{ref}/diff', requestParams); return ( data?.map((item) => ({ - path: item.new_path || item.old_path!, - previousPath: item.old_path, + path: normalizePath(item.new_path || item.old_path!), + previousPath: item.old_path ? normalizePath(item.old_path) : undefined, status: item.new_file ? FileChangeStatus.Added : item.deleted_file @@ -373,8 +374,8 @@ export class GitLab1sDataSource extends DataSource { ); return data.changes.map((item) => ({ - path: item.new_path, - previousPath: item.old_path, + path: normalizePath(item.new_path), + previousPath: item.old_path ? normalizePath(item.old_path) : undefined, status: item.new_file ? FileChangeStatus.Added : item.deleted_file @@ -388,7 +389,7 @@ export class GitLab1sDataSource extends DataSource { @trySourcegraphApiFirst async provideFileBlameRanges(repo: string, ref: string, path: string): Promise { const fetcher = GitLabFetcher.getInstance(); - const requestParams = { repo, ref, path }; + const requestParams = { repo, ref, path: trimStart(path, '/') }; const { data } = await fetcher.request( 'GET /projects/{repo}/repository/files/{path}/blame?ref={ref}', requestParams, diff --git a/extensions/github1s/src/adapters/gitlab1s/parse-path.ts b/extensions/github1s/src/adapters/gitlab1s/parse-path.ts index 7dc02a531..9e108ba6d 100644 --- a/extensions/github1s/src/adapters/gitlab1s/parse-path.ts +++ b/extensions/github1s/src/adapters/gitlab1s/parse-path.ts @@ -164,6 +164,6 @@ export const parseGitLabPath = async (path: string): Promise => { repo: DEFAULT_REPO, ref: await getDefaultBranch(DEFAULT_REPO), pageType: PageType.Tree, - filePath: '', + filePath: '/', }; }; diff --git a/extensions/github1s/src/adapters/gitlab1s/router-parser.ts b/extensions/github1s/src/adapters/gitlab1s/router-parser.ts index 2e1809fbd..40aca6882 100644 --- a/extensions/github1s/src/adapters/gitlab1s/router-parser.ts +++ b/extensions/github1s/src/adapters/gitlab1s/router-parser.ts @@ -22,12 +22,12 @@ export class GitLab1sRouterParser extends adapterTypes.RouterParser { } buildTreePath(repo: string, ref?: string, filePath?: string): string { - return ref ? (filePath ? `/${repo}/-/tree/${ref}/${filePath}` : `/${repo}/-/tree/${ref}`) : `/${repo}`; + return ref ? `/${repo}/-/tree/${ref}${filePath && filePath !== '/' ? filePath : ''}` : `/${repo}`; } buildBlobPath(repo: string, ref: string, filePath: string, startLine?: number, endLine?: number): string { const hash = startLine ? (endLine ? `#L${startLine}-L${endLine}` : `#L${startLine}`) : ''; - return `/${repo}/-/blob/${ref}/${filePath}${hash}`; + return `/${repo}/-/blob/${ref}${filePath}${hash}`; } buildCommitListPath(repo: string): string { diff --git a/extensions/github1s/src/adapters/npmjs1s/data-source.ts b/extensions/github1s/src/adapters/npmjs1s/data-source.ts index 8e9b3606c..4fac6312e 100644 --- a/extensions/github1s/src/adapters/npmjs1s/data-source.ts +++ b/extensions/github1s/src/adapters/npmjs1s/data-source.ts @@ -6,6 +6,7 @@ import { CommonQueryOptions, DataSource, Directory, DirectoryEntry, File, FileType, Tag } from '../types'; import { matchSorter } from 'match-sorter'; import * as dayjs from 'dayjs'; +import { normalizePath } from '@/helpers/util'; type PackageFile = { path: string; @@ -23,14 +24,13 @@ type PackageEntry = PackageFile | PackageDirectory; type PackageVersion = { name: string; tag?: string; time?: Date }; -const retrieveFiles = (files: PackageEntry[], pathDeep: number, recursive: boolean) => { +const retrieveFiles = (files: PackageEntry[], recursive: boolean) => { const entries: DirectoryEntry[] = []; for (const item of files) { const fileType = item.type === 'directory' ? FileType.Directory : FileType.File; - const filePath = item.path.split(/\/+/).filter(Boolean).slice(pathDeep).join('/'); - entries.push({ type: fileType, path: filePath }); + entries.push({ type: fileType, path: normalizePath(item.path) }); if (recursive && item.type === 'directory' && item.files?.length) { - entries.push(...retrieveFiles(item.files, pathDeep, recursive)); + entries.push(...retrieveFiles(item.files, recursive)); } } return entries; @@ -70,12 +70,12 @@ export class Npmjs1sDataSource extends DataSource { }, await this.getPackageFiles(packageName, version), ); - const entries = parentFiles ? retrieveFiles(parentFiles, pathParts.length, recursive) : []; + const entries = parentFiles ? retrieveFiles(parentFiles, recursive) : []; return { entries, truncated: false }; } async provideFile(packageName: string, version: string, path: string): Promise { - const response = await fetch(`https://unpkg.com/${packageName}@${version}/${path}`); + const response = await fetch(`https://unpkg.com/${packageName}@${version}${path}`); return { content: new Uint8Array(await response.arrayBuffer()) }; } diff --git a/extensions/github1s/src/adapters/npmjs1s/router-parser.ts b/extensions/github1s/src/adapters/npmjs1s/router-parser.ts index 76f635782..5b67a229a 100644 --- a/extensions/github1s/src/adapters/npmjs1s/router-parser.ts +++ b/extensions/github1s/src/adapters/npmjs1s/router-parser.ts @@ -10,7 +10,7 @@ export const parseNpmPath = async (path: string): Promise => { const pathParts = parsePath(path).pathname?.split('/').filter(Boolean) || []; if (!pathParts.length) { - return { pageType: PageType.Tree, repo: 'lodash', ref: 'latest', filePath: '' }; + return { pageType: PageType.Tree, repo: 'lodash', ref: 'latest', filePath: '/' }; } const trimedParts = pathParts[0] === 'package' ? pathParts.slice(1) : pathParts; @@ -20,7 +20,7 @@ export const parseNpmPath = async (path: string): Promise => { const packageVersion = trimedParts[packagePartsLength] === 'v' ? trimedParts[packagePartsLength + 1] || 'latest' : 'latest'; - return { pageType: PageType.Tree as const, repo: packageName, ref: packageVersion, filePath: '' }; + return { pageType: PageType.Tree as const, repo: packageName, ref: packageVersion, filePath: '/' }; }; export class Npmjs1sRouterParser extends RouterParser { diff --git a/extensions/github1s/src/adapters/ossinsight/data-source.ts b/extensions/github1s/src/adapters/ossinsight/data-source.ts index 3f03ebcd4..a9a19278e 100644 --- a/extensions/github1s/src/adapters/ossinsight/data-source.ts +++ b/extensions/github1s/src/adapters/ossinsight/data-source.ts @@ -129,7 +129,7 @@ export class OSSInsightDataSource extends DataSource { } async provideDirectory(repo: string, ref: string, path: string, recursive?: boolean): Promise { - const walk = async (item: StructureItem | undefined, recursive = false, basePath = '') => { + const walk = async (item: StructureItem | undefined, recursive = false, basePath = '/') => { const directoryEntires: Directory['entries'] = []; for (const child of await this.getStructureItemChildren(item)) { const currentPath = joinPath(basePath, child.name); @@ -143,7 +143,7 @@ export class OSSInsightDataSource extends DataSource { return { truncated: false, - entries: await walk(await this.resolveStructureItem(path), recursive), + entries: await walk(await this.resolveStructureItem(path), recursive, path), }; } diff --git a/extensions/github1s/src/adapters/ossinsight/router-parser.ts b/extensions/github1s/src/adapters/ossinsight/router-parser.ts index 8da4a14ef..043e41de3 100644 --- a/extensions/github1s/src/adapters/ossinsight/router-parser.ts +++ b/extensions/github1s/src/adapters/ossinsight/router-parser.ts @@ -7,6 +7,7 @@ import { parsePath } from 'history'; import * as queryString from 'query-string'; import * as adapterTypes from '../types'; import { GitHub1sRouterParser } from '../github1s/router-parser'; +import { normalizePath } from '@/helpers/util'; export class OSSInsightRouterParser extends GitHub1sRouterParser { protected static instance: OSSInsightRouterParser | null = null; @@ -20,7 +21,7 @@ export class OSSInsightRouterParser extends GitHub1sRouterParser { async parsePath(path: string): Promise { const { path: pathsOrNull } = queryString.parse((parsePath(path).search || '').slice(1)); - const filePath = (Array.isArray(pathsOrNull) ? pathsOrNull[0] : pathsOrNull) || ''; + const filePath = normalizePath((Array.isArray(pathsOrNull) ? pathsOrNull[0] : pathsOrNull) || ''); const pageType = filePath.endsWith('.md') ? adapterTypes.PageType.Blob : adapterTypes.PageType.Tree; return { pageType, repo: '', ref: '', filePath }; } diff --git a/extensions/github1s/src/adapters/sourcegraph/data-source.ts b/extensions/github1s/src/adapters/sourcegraph/data-source.ts index 8fd002c68..d46302920 100644 --- a/extensions/github1s/src/adapters/sourcegraph/data-source.ts +++ b/extensions/github1s/src/adapters/sourcegraph/data-source.ts @@ -3,7 +3,6 @@ * @author netcon */ -import { joinPath } from '@/helpers/util'; import { matchSorter } from 'match-sorter'; import { Branch, @@ -35,6 +34,7 @@ import { getSymbolReferences } from './reference'; import { getRepository } from './repository'; import { getTextSearchResults } from './search'; import { decorate, memorize } from '@/helpers/func'; +import { normalizePath, trimStart } from '@/helpers/util'; type SupportedPlatform = 'github' | 'gitlab' | 'bitbucket'; @@ -71,26 +71,24 @@ export class SourcegraphDataSource extends DataSource { } async provideDirectory(repo: string, ref: string, path: string, recursive = false): Promise { - const directories = await readDirectory(this.buildRepository(repo), ref, path, recursive); + const directories = await readDirectory(this.buildRepository(repo), ref, trimStart(path, '/'), recursive); directories.entries.forEach((entry) => { - const mapKey = `${repo} ${ref} ${joinPath(path, entry.path)}`; + const mapKey = `${repo} ${ref} ${entry.path}`; this.fileTypeMap.set(mapKey, entry.type); }); return directories; } async detectPathFileType(repo: string, ref: string, path: string) { - const pathParts = path.split('/').filter(Boolean); - const trimmedPath = pathParts.join('/'); - if (!trimmedPath) { + if (path === '/') { return FileType.Directory; } - const mapKey = `${repo} ${ref} ${trimmedPath}`; + const mapKey = `${repo} ${ref} ${path}`; if (this.fileTypeMap.has(mapKey)) { return this.fileTypeMap.get(mapKey)!; } - await this.provideDirectory(repo, ref, pathParts.slice(0, -1).join('/'), false); - return this.fileTypeMap.get(trimmedPath) || FileType.File; + await this.provideDirectory(repo, ref, normalizePath(path.split('/').slice(0, -1).join('/')), false); + return this.fileTypeMap.get(mapKey) || FileType.File; } async provideRepository(repo: string) { @@ -101,6 +99,7 @@ export class SourcegraphDataSource extends DataSource { } async provideFile(repo: string, ref: string, path: string): Promise { + const apiPath = trimStart(path, '/'); // sourcegraph api break binary files and text coding, so we use github api first here if (this.platform === 'github') { // For GitHub repositories, request GitHub User Content API first (it seems no Rate Limit), @@ -108,13 +107,13 @@ export class SourcegraphDataSource extends DataSource { // Content API goes wrong, then try Sourcegraph API. Use `try catch` because if fallback to // GitHub REST API may trigger a pop-up window to request authentication for anonymous users. try { - return fetch(encodeURI(`https://raw.githubusercontent.com/${repo}/${ref}/${path}`)) + return fetch(encodeURI(`https://raw.githubusercontent.com/${repo}/${ref}/${apiPath}`)) .then((response) => (response.ok ? response.arrayBuffer() : Promise.reject({ response }))) .then((buffer) => ({ content: new Uint8Array(buffer) })); } catch {} } // TODO: support binary files for other platforms - const { content } = await readFile(this.buildRepository(repo), ref, path); + const { content } = await readFile(this.buildRepository(repo), ref, apiPath); return { content: this.textEncoder.encode(content) }; } @@ -132,10 +131,10 @@ export class SourcegraphDataSource extends DataSource { async extractRefPath(repo: string, refAndPath: string): Promise<{ ref: string; path: string }> { if (!refAndPath) { - return { ref: await this.getDefaultBranch(repo), path: '' }; + return { ref: await this.getDefaultBranch(repo), path: '/' }; } if (refAndPath.match(/^HEAD(\/.*)?$/i)) { - return { ref: 'HEAD', path: refAndPath.slice(5) }; + return { ref: 'HEAD', path: normalizePath(refAndPath.slice(5)) }; } if (!this.matchedRefsMap.has(repo)) { this.matchedRefsMap.set(repo, []); @@ -143,13 +142,13 @@ export class SourcegraphDataSource extends DataSource { const matchPathRef = (ref) => refAndPath.startsWith(`${ref}/`) || refAndPath === ref; const pathRef = this.matchedRefsMap.get(repo)?.find(matchPathRef); if (pathRef) { - return { ref: pathRef, path: refAndPath.slice(pathRef.length + 1) }; + return { ref: pathRef, path: normalizePath(refAndPath.slice(pathRef.length + 1)) }; } const { branches, tags } = await this.prepareAllRefs(repo); const exactRef = [...branches, ...tags].map((item) => item.name).find(matchPathRef); const ref = exactRef || refAndPath.split('/')[0] || 'HEAD'; exactRef && this.matchedRefsMap.get(repo)?.push(ref); - return { ref, path: refAndPath.slice(ref.length + 1) }; + return { ref, path: normalizePath(refAndPath.slice(ref.length + 1)) }; } async provideBranches(repo: string, options?: CommonQueryOptions): Promise { @@ -184,17 +183,21 @@ export class SourcegraphDataSource extends DataSource { query: TextSearchQuery, options: TextSearchOptions, ): Promise { - return getTextSearchResults(this.buildRepository(repo), ref, query, options); + const results = await getTextSearchResults(this.buildRepository(repo), ref, query, options); + return { + ...results, + results: results.results.map((result) => ({ ...result, path: normalizePath(result.path) })), + }; } async provideCommits(repo: string, options?: CommitsQueryOptions): Promise<(Commit & { files?: ChangedFile[] })[]> { let commits = await getCommits( this.buildRepository(repo), options?.from || 'HEAD', - options?.path, + options?.path === undefined ? undefined : trimStart(options.path, '/'), options?.pageSize ? options.pageSize * (options.page || 1) : undefined, ); - if (options?.path && commits.length) { + if (options?.path && options.path !== '/' && commits.length) { // find the latested that related the `options.path` file const changedFiles = await this.provideCommitChangedFiles(repo, commits[0].sha); commits = changedFiles.find((file) => file.path === options.path) ? commits : commits.slice(1); @@ -207,11 +210,15 @@ export class SourcegraphDataSource extends DataSource { } async provideCommitChangedFiles(repo: string, ref: string, _options?: CommonQueryOptions): Promise { - return compareCommits(this.buildRepository(repo), `${ref}~`, ref); + return (await compareCommits(this.buildRepository(repo), `${ref}~`, ref)).map((file) => ({ + ...file, + path: normalizePath(file.path), + previousPath: file.previousPath ? normalizePath(file.previousPath) : undefined, + })); } async provideFileBlameRanges(repo: string, ref: string, path: string): Promise { - return getFileBlameRanges(this.buildRepository(repo), ref, path); + return getFileBlameRanges(this.buildRepository(repo), ref, trimStart(path, '/')); } async provideSymbolDefinitions( @@ -222,7 +229,10 @@ export class SourcegraphDataSource extends DataSource { character: number, symbol: string, ): Promise { - return getSymbolDefinitions(this.buildRepository(repo), ref, path, line, character, symbol); + const apiPath = trimStart(path, '/'); + return getSymbolDefinitions(this.buildRepository(repo), ref, apiPath, line, character, symbol).then((locations) => + locations.map((location) => ({ ...location, path: normalizePath(location.path) })), + ); } async provideSymbolReferences( @@ -233,7 +243,10 @@ export class SourcegraphDataSource extends DataSource { character: number, symbol: string, ): Promise { - return getSymbolReferences(this.buildRepository(repo), ref, path, line, character, symbol); + const apiPath = trimStart(path, '/'); + return getSymbolReferences(this.buildRepository(repo), ref, apiPath, line, character, symbol).then((locations) => + locations.map((location) => ({ ...location, path: normalizePath(location.path) })), + ); } async provideSymbolHover( diff --git a/extensions/github1s/src/adapters/sourcegraph/file.ts b/extensions/github1s/src/adapters/sourcegraph/file.ts index 6366b43b9..280c1bda5 100644 --- a/extensions/github1s/src/adapters/sourcegraph/file.ts +++ b/extensions/github1s/src/adapters/sourcegraph/file.ts @@ -6,6 +6,7 @@ import { gql } from '@apollo/client/core'; import { querySourcegraphRepository } from './common'; import { Directory, FileType } from '../types'; +import { normalizePath } from '@/helpers/util'; const FILE_COUNT_LIMIT = 50000; @@ -39,12 +40,11 @@ export const readDirectory = async ( variables: { repository, ref, path, recursive }, }); const files = repositoryData.commit?.tree?.entries || []; - const pathParts = path.split('/').filter(Boolean); return { entries: files.map((file) => ({ - path: file.path.split('/').filter(Boolean).slice(pathParts.length).join('/'), + path: normalizePath(file.path), type: file.isDirectory ? FileType.Directory : file.submodule ? FileType.Submodule : FileType.File, - commitSha: file.submodule?.sha, + commitSha: file.submodule?.commit, })), truncated: files.length >= FILE_COUNT_LIMIT, }; diff --git a/extensions/github1s/src/adapters/types.ts b/extensions/github1s/src/adapters/types.ts index f53e0c380..ecc563d99 100644 --- a/extensions/github1s/src/adapters/types.ts +++ b/extensions/github1s/src/adapters/types.ts @@ -169,10 +169,9 @@ export type SymbolReferences = CodeLocation[]; export type SymbolHover = { markdown: string }; +// All repository path parameters and return values start with '/'. export class DataSource { // if `recursive` is true, it should try to return all subtrees - // the returned Directory.entries.path is relative the `path` in arguments, - // so if `recursive` is false, the returned path should be the file name provideDirectory(repo: string, ref: string, path: string, recursive = false): Promisable { return null; } @@ -344,7 +343,7 @@ export type RouterState = { repo: string; ref: string } & ( export class RouterParser { // parse giving path (starts with '/', may includes search and hash) to Router state, parsePath(path: string): Promisable { - return { repo: '', ref: 'HEAD', pageType: PageType.Tree, filePath: '' }; + return { repo: '', ref: 'HEAD', pageType: PageType.Tree, filePath: '/' }; } // build the tree page path diff --git a/extensions/github1s/src/changes/files.ts b/extensions/github1s/src/changes/files.ts index 56c9f42cd..8d4b0ddd7 100644 --- a/extensions/github1s/src/changes/files.ts +++ b/extensions/github1s/src/changes/files.ts @@ -22,18 +22,10 @@ interface VSCodeChangedFile { export const getCodeReviewChangedFiles = async ( codeReview: adapterTypes.CodeReview & { sourceSha: string; targetSha: string }, ) => { - const scheme = adapterManager.getCurrentScheme(); - const { repo } = await router.getState(); - const baseRootUri = vscode.Uri.parse('').with({ - scheme: scheme, - authority: `${repo}+${codeReview.targetSha}`, - path: '/', - }); - const headRootUri = baseRootUri.with({ - authority: `${repo}+${codeReview.sourceSha}`, - }); + const baseRootUri = router.buildUri({ ref: codeReview.targetSha }); + const headRootUri = router.buildUri({ ref: codeReview.sourceSha }, baseRootUri); - const repository = Repository.getInstance(scheme, repo); + const repository = Repository.getCurrentInstance(); const changedFiles = await repository.getCodeReviewChangedFiles(codeReview.id); return changedFiles.map((changedFile) => { @@ -42,30 +34,22 @@ export const getCodeReviewChangedFiles = async ( const baseFilePath = changedFile.previousPath || changedFile.path; const headFilePath = changedFile.path; return { - baseFileUri: vscode.Uri.joinPath(baseRootUri, baseFilePath), - headFileUri: vscode.Uri.joinPath(headRootUri, headFilePath), + baseFileUri: baseRootUri.with({ path: baseFilePath }), + headFileUri: headRootUri.with({ path: headFilePath }), status: changedFile.status, }; }); }; export const getCommitChangedFiles = async (commit: adapterTypes.Commit) => { - const currentAdapter = adapterManager.getCurrentAdapter(); - const scheme = currentAdapter.scheme; - const { repo } = await router.getState(); // if the commit.parents is more than one element // the parents[1].sha should be the merge source commitSha // so we use the parents[0].sha as the parent commitSha - const baseRef = commit?.parents?.[0]; - const baseRootUri = vscode.Uri.parse('').with({ - scheme: currentAdapter.scheme, - authority: `${repo}+${baseRef || 'HEAD'}`, - path: '/', - }); - const headRootUri = baseRootUri.with({ - authority: `${repo}+${commit.sha || 'HEAD'}`, - }); - const repository = Repository.getInstance(scheme, repo); + const parentCommitSha = commit?.parents?.[0] || ''; + const baseRootUri = router.buildUri({ ref: parentCommitSha }); + const headRootUri = router.buildUri({ ref: commit.sha }, baseRootUri); + + const repository = Repository.getCurrentInstance(); const changedFiles = await repository.getCommitChangedFiles(commit.sha); return changedFiles.map((commitFile) => { @@ -74,26 +58,25 @@ export const getCommitChangedFiles = async (commit: adapterTypes.Commit) => { const baseFilePath = commitFile.previousPath || commitFile.path; const headFilePath = commitFile.path; return { - baseFileUri: vscode.Uri.joinPath(baseRootUri, baseFilePath), - headFileUri: vscode.Uri.joinPath(headRootUri, headFilePath), + baseFileUri: baseRootUri.with({ path: baseFilePath }), + headFileUri: headRootUri.with({ path: headFilePath }), status: commitFile.status, }; }); }; export const getChangedFiles = async (): Promise => { - const routerState = await router.getState(); - const scheme = adapterManager.getCurrentScheme(); + const routerState = router.getState(); // code review page if (routerState.pageType === adapterTypes.PageType.CodeReview) { - const repository = Repository.getInstance(scheme, routerState.repo); + const repository = Repository.getInstance(routerState.scheme, routerState.repo); const codeReview = await repository.getCodeReviewItem(routerState.codeReviewId); return codeReview ? getCodeReviewChangedFiles(codeReview) : []; } // commit page else if (routerState.pageType === adapterTypes.PageType.Commit) { - const repository = Repository.getInstance(scheme, routerState.repo); + const repository = Repository.getInstance(routerState.scheme, routerState.repo); const commit = await repository.getCommitItem(routerState.commitSha); return commit ? getCommitChangedFiles(commit) : []; } @@ -108,8 +91,8 @@ export const getChangedFileDiffTitle = ( ) => { const baseFileName = basename(baseFileUri.path); const headFileName = basename(headFileUri.path); - const [_repo, baseCommitSha] = baseFileUri.authority.split('+'); - const [__repo, headCommitSha] = headFileUri.authority.split('+'); + const { ref: baseCommitSha } = router.parseUri(baseFileUri); + const { ref: headCommitSha } = router.parseUri(headFileUri); const baseFileLabel = `${baseFileName} (${baseCommitSha?.slice(0, 7)})`; const headFileLabel = `${headFileName} (${headCommitSha?.slice(0, 7)})`; diff --git a/extensions/github1s/src/changes/index.ts b/extensions/github1s/src/changes/index.ts index 8847d5b5e..4a3456f8b 100644 --- a/extensions/github1s/src/changes/index.ts +++ b/extensions/github1s/src/changes/index.ts @@ -7,11 +7,9 @@ import * as vscode from 'vscode'; import * as adapterTypes from '@/adapters/types'; import { GitHub1sQuickDiffProvider } from './quick-diff'; import { getChangedFileDiffCommand, getChangedFiles } from './files'; -import adapterManager from '@/adapters/manager'; export const updateSourceControlChanges = (() => { - const rootUri = vscode.Uri.parse('').with({ scheme: adapterManager.getCurrentScheme() }); - const sourceControl = vscode.scm.createSourceControl('github1s', 'GitHub1s', rootUri); + const sourceControl = vscode.scm.createSourceControl('github1s', 'GitHub1s'); const changesGroup = sourceControl.createResourceGroup('changes', 'Changes'); sourceControl.quickDiffProvider = new GitHub1sQuickDiffProvider(); @@ -20,7 +18,7 @@ export const updateSourceControlChanges = (() => { changesGroup.resourceStates = changedFiles.map((changedFile) => { return { - resourceUri: changedFile.headFileUri, + resourceUri: changedFile.headFileUri.with({ authority: '' }), decorations: { strikeThrough: changedFile.status === adapterTypes.FileChangeStatus.Removed, tooltip: changedFile.status, diff --git a/extensions/github1s/src/changes/quick-diff.ts b/extensions/github1s/src/changes/quick-diff.ts index 507b89b38..d620b218c 100644 --- a/extensions/github1s/src/changes/quick-diff.ts +++ b/extensions/github1s/src/changes/quick-diff.ts @@ -12,11 +12,9 @@ import * as adapterTypes from '@/adapters/types'; // get the original source uri when the `routerState.pageType` is `PageType.PULL` const getOriginalResourceForPull = async (uri: vscode.Uri, codeReviewId: string): Promise => { - const routeState = await router.getState(); - const currentScheme = adapterManager.getCurrentScheme(); - const repository = Repository.getInstance(currentScheme, routeState.repo); + const repository = await Repository.getCurrentInstance(); const codeReviewFiles = await repository.getCodeReviewChangedFiles(codeReviewId); - const changedFile = codeReviewFiles?.find((changedFile) => changedFile.path === uri.path.slice(1)); + const changedFile = codeReviewFiles?.find((changedFile) => changedFile.path === uri.path); if ( !changedFile || @@ -31,19 +29,17 @@ const getOriginalResourceForPull = async (uri: vscode.Uri, codeReviewId: string) return null; } - const originalAuthority = `${routeState.repo}+${codeReview!.targetSha}`; - const originalPath = changedFile.previousPath ? `/${changedFile.previousPath}` : uri.path; - - return uri.with({ authority: originalAuthority, path: originalPath }); + return router.buildUri({ + ref: codeReview.targetSha, + path: changedFile.previousPath || uri.path, + }); }; // get the original source uri when the `routerState.pageType` is `PageType.COMMIT` const getOriginalResourceForCommit = async (uri: vscode.Uri, commitSha: string) => { - const routeState = await router.getState(); - const currentScheme = adapterManager.getCurrentScheme(); - const repository = Repository.getInstance(currentScheme, routeState.repo); + const repository = Repository.getCurrentInstance(); const commitFiles = await repository.getCommitChangedFiles(commitSha); - const changedFile = commitFiles?.find((changedFile) => changedFile.path === uri.path.slice(1)); + const changedFile = commitFiles?.find((changedFile) => changedFile.path === uri.path); if ( !changedFile || @@ -59,33 +55,28 @@ const getOriginalResourceForCommit = async (uri: vscode.Uri, commitSha: string) return emptyFileUri; } - const originalAuthority = `${routeState.repo}+${parentCommitSha}`; - const originalPath = changedFile.previousPath ? `/${changedFile.previousPath}` : uri.path; - - return uri.with({ authority: originalAuthority, path: originalPath }); + return router.buildUri({ + ref: parentCommitSha, + path: changedFile.previousPath || uri.path, + }); }; export class GitHub1sQuickDiffProvider implements vscode.QuickDiffProvider { provideOriginalResource(uri: vscode.Uri, _token: vscode.CancellationToken): vscode.ProviderResult { - if (uri.scheme !== adapterManager.getCurrentScheme()) { + const routerState = router.getState(); + // only the file belong to current workspace could be provided a quick diff + if (uri.scheme !== routerState.scheme || uri.authority) { return null; } - return router.getState().then(async (routerState) => { - // only the file belong to current authority could be provided a quick diff - if (uri.authority && uri.authority !== (await router.getAuthority())) { - return null; - } - - if (routerState.pageType === adapterTypes.PageType.CodeReview) { - return getOriginalResourceForPull(uri, routerState.codeReviewId); - } + if (routerState.pageType === adapterTypes.PageType.CodeReview) { + return getOriginalResourceForPull(uri, routerState.codeReviewId); + } - if (routerState.pageType === adapterTypes.PageType.Commit) { - return getOriginalResourceForCommit(uri, routerState.commitSha); - } + if (routerState.pageType === adapterTypes.PageType.Commit) { + return getOriginalResourceForCommit(uri, routerState.commitSha); + } - return null; - }); + return null; } } diff --git a/extensions/github1s/src/commands/blame.ts b/extensions/github1s/src/commands/blame.ts index 2c67eb155..837a94997 100644 --- a/extensions/github1s/src/commands/blame.ts +++ b/extensions/github1s/src/commands/blame.ts @@ -195,18 +195,16 @@ class EditorGitBlame { } async getBlameRanges(): Promise { - const filePath = this.editor.document?.uri.path; - const fileAuthority = this.editor.document?.uri.authority || (await router.getAuthority()); - const [repo, ref] = fileAuthority.split('+').filter(Boolean); - const scheme = adapterManager.getCurrentScheme(); + const { scheme, repo, ref, path } = router.parseUri(this.editor.document?.uri); const repository = Repository.getInstance(scheme, repo); - return filePath ? repository.getFileBlameRanges(ref, filePath.slice(1)) : []; + return path.length > 1 ? repository.getFileBlameRanges(ref, path) : []; } async open() { this.refreshDisposables.forEach((disposable) => disposable.dispose()); setVSCodeContext('github1s:features:gutterBlame:open', true); - const { platformName } = adapterManager.getCurrentAdapter(); + const { scheme } = router.parseUri(this.editor.document.uri); + const { platformName } = adapterManager.getAdapter(scheme); (await this.getBlameRanges()).forEach((blameRange) => { const hoverMessage = createCommitMessagePreviewMarkdown(blameRange, platformName); diff --git a/extensions/github1s/src/commands/code-review.ts b/extensions/github1s/src/commands/code-review.ts index 77e746ea8..8c943d312 100644 --- a/extensions/github1s/src/commands/code-review.ts +++ b/extensions/github1s/src/commands/code-review.ts @@ -44,10 +44,10 @@ const commandSwitchToCodeReview = async (codeReviewItemOrId?: string | CodeRevie ? codeReviewItemOrId : codeReviewItemOrId.codeReview.id : ''; + const { repo } = router.getState(); const adapter = adapterManager.getCurrentAdapter(); - const { repo } = await router.getState(); const typeName = CodeReviewTypeName[adapter.codeReviewType || CodeReviewType.CodeReview]; - const repository = Repository.getInstance(adapter.scheme, repo); + const repository = Repository.getCurrentInstance(); // if the a codeReviewId isn't provided, use quickInput if (!codeReviewId) { @@ -90,7 +90,7 @@ const commandSwitchToCodeReview = async (codeReviewItemOrId?: string | CodeRevie } } - const routerParser = await router.resolveParser(); + const routerParser = router.getParser(); (await checkCodeReviewExists(repo, codeReviewId!)) && router.replace(await routerParser.buildCodeReviewPath(repo, codeReviewId!)); }; @@ -103,8 +103,8 @@ const commandOpenCodeReviewOnOfficialPage = async (codeReviewItemOrId?: string | : codeReviewItemOrId.codeReview.id : ''; if (codeReviewId) { - const { repo } = await router.getState(); - const routerParser = await router.resolveParser(); + const { repo } = router.getState(); + const routerParser = router.getParser(); const codeReviewPath = await routerParser.buildCodeReviewPath(repo, codeReviewId); const codeReviewLink = await routerParser.buildExternalLink(codeReviewPath); return vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(codeReviewLink)); diff --git a/extensions/github1s/src/commands/commit.ts b/extensions/github1s/src/commands/commit.ts index 1ae1ccdb2..d7fa85d42 100644 --- a/extensions/github1s/src/commands/commit.ts +++ b/extensions/github1s/src/commands/commit.ts @@ -31,9 +31,8 @@ const commandSwitchToCommit = async (commitItemOrSha?: string | CommitTreeItem) ? commitItemOrSha : commitItemOrSha.commit.sha : ''; - const adapter = adapterManager.getCurrentAdapter(); - const { repo } = await router.getState(); - const repository = Repository.getInstance(adapter.scheme, repo); + const { repo } = router.getState(); + const repository = Repository.getCurrentInstance(); // if the a commitSha isn't provided, use quickInput if (!commitSha) { @@ -76,7 +75,7 @@ const commandSwitchToCommit = async (commitItemOrSha?: string | CommitTreeItem) } } - const routerParser = await router.resolveParser(); + const routerParser = router.getParser(); if (await checkCommitExists(repo, commitSha!)) { router.replace(await routerParser.buildCommitPath(repo, commitSha!)); } @@ -87,12 +86,11 @@ const commandDiffCommitFile = async (commitItem: CommitTreeItem) => { if (!commitSha) { return; } - const { repo } = await router.getState(); const activeDocumentUri = vscode.window.activeTextEditor?.document?.uri; - const fileUri = activeDocumentUri?.with({ - authority: `${repo}+${commitSha}`, - query: '', - }); + if (!activeDocumentUri) { + return; + } + const fileUri = router.buildUri({ ref: commitSha }, activeDocumentUri).with({ query: '' }); return vscode.commands.executeCommand('github1s.commands.openFilePreviousRevision', fileUri); }; @@ -104,8 +102,8 @@ const commandOpenCommitOnOfficialPage = async (commitItemOrSha?: string | Commit : commitItemOrSha.commit.sha : ''; if (commitSha) { - const { repo } = await router.getState(); - const routerParser = await router.resolveParser(); + const { repo } = router.getState(); + const routerParser = router.getParser(); const commitPath = await routerParser.buildCommitPath(repo, commitSha); const commitLink = await routerParser.buildExternalLink(commitPath); return vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(commitLink)); diff --git a/extensions/github1s/src/commands/editor.ts b/extensions/github1s/src/commands/editor.ts index 80c793ae7..24db76644 100644 --- a/extensions/github1s/src/commands/editor.ts +++ b/extensions/github1s/src/commands/editor.ts @@ -7,7 +7,6 @@ import * as vscode from 'vscode'; import * as queryString from 'query-string'; import router from '@/router'; import { emptyFileUri } from '@/providers'; -import { basename } from '@/helpers/util'; import { FileChangeStatus } from '@/adapters/types'; import { Repository } from '@/repository'; import { getChangedFiles, getChangedFileDiffCommand, getChangedFileDiffTitle } from '@/changes/files'; @@ -37,21 +36,7 @@ const commandDiffChangedFile = async (fileUri: vscode.Uri) => { }; const openFileToEditor = async (fileUri) => { - const isCurrentAuthority = fileUri.authority === (await router.getAuthority()); - - // In order to make the file explorer focus corresponding file when - // the `fileUri.authority` equals `current authority`, set the - // `fileUri.authority` to '' in this case - const targetFileUri = isCurrentAuthority ? fileUri.with({ authority: '' }) : fileUri; - - let editorLabel: string | undefined = undefined; - if (!isCurrentAuthority) { - // the authority here should be `{repo}+{commitSha}` - const [_repo, commitSha] = targetFileUri.authority.split('+'); - editorLabel = `${basename(targetFileUri.path)} (${commitSha.slice(0, 7)})`; - } - - return vscode.commands.executeCommand('vscode.open', targetFileUri, { preview: false }, editorLabel); + return vscode.commands.executeCommand('vscode.open', fileUri, { preview: false }); }; // open the left file in the diff editor title @@ -69,14 +54,12 @@ const commandDiffViewOpenRightFile = async (fileUri: vscode.Uri) => { // get the file uri with the concrete commit sha, the `ref` in // `fileUri.authority` maybe newer but not related this file const getConcreteFileUri = async (fileUri: vscode.Uri) => { - // the `fileUri.authority` maybe empty, fallback to router.getAuthority() in this case - const fileAuthority = fileUri.authority || (await router.getAuthority()); - const [repo, ref] = fileAuthority.split('+').filter(Boolean); - const repository = Repository.getInstance(fileUri.scheme, repo); - const commit = await repository.getFileLatestCommit(ref, fileUri.path.slice(1)); - const latestCommitSha = commit?.sha || (await repository.getCommitItem(ref))?.sha || 'HEAD'; - - return fileUri.with({ authority: `${repo}+${latestCommitSha}` }); + const { ref, path } = router.parseUri(fileUri); + const repository = Repository.getInstanceByUri(fileUri); + const commit = await repository.getFileLatestCommit(ref, path); + const latestCommitSha = commit?.sha || (await repository.getCommitItem(ref))?.sha; + + return router.buildUri({ ref: latestCommitSha }, fileUri); }; // show the file's diff between current commit and previous commit @@ -87,15 +70,15 @@ const commandOpenFilePreviousRevision = async (fileUri: vscode.Uri) => { // a normal file editor (not a diff editor), just use `fileUri` in this case queryBaseUriStr ? vscode.Uri.parse(queryBaseUriStr as string) : fileUri, ); - const [repo, rightCommitSha] = rightFileUri.authority.split('+').filter(Boolean); + const { repo, ref: rightCommitSha } = router.parseUri(rightFileUri); - const repository = Repository.getInstance(fileUri.scheme, repo); - const leftCommit = await repository.getPreviousCommit(rightCommitSha, fileUri.path.slice(1)); + const repository = Repository.getInstanceByUri(rightFileUri); + const leftCommit = await repository.getPreviousCommit(rightCommitSha, rightFileUri.path); // if we can't find previous commit, use the `emptyFileUri` as the leftFileUri - const leftFileUri = leftCommit ? rightFileUri.with({ authority: `${repo}+${leftCommit.sha}` }) : emptyFileUri; + const leftFileUri = leftCommit ? router.buildUri({ ref: leftCommit.sha }, rightFileUri) : emptyFileUri; const changedStatus = leftCommit ? FileChangeStatus.Modified : FileChangeStatus.Added; - const hasNextRevision = !!(await repository.getNextCommit(rightCommitSha, rightFileUri.path.slice(1))); + const hasNextRevision = !!(await repository.getNextCommit(rightCommitSha, rightFileUri.path)); const query = queryString.stringify({ base: leftFileUri.with({ query: '' }).toString(), @@ -118,16 +101,16 @@ const commandOpenFilePreviousRevision = async (fileUri: vscode.Uri) => { const commandOpenFileNextRevision = async (fileUri: vscode.Uri) => { const leftFileUri = await getConcreteFileUri(fileUri); - const [repo, leftCommitSha] = leftFileUri.authority.split('+').filter(Boolean); - const repository = Repository.getInstance(fileUri.scheme, repo); - const rightCommit = await repository.getNextCommit(leftCommitSha, fileUri.path.slice(1)); + const { ref: leftCommitSha } = router.parseUri(leftFileUri); + const repository = Repository.getInstanceByUri(leftFileUri); + const rightCommit = await repository.getNextCommit(leftCommitSha, leftFileUri.path); if (!rightCommit) { return vscode.window.showInformationMessage('There is no next commit found.'); } - const rightFileUri = leftFileUri.with({ authority: `${repo}+${rightCommit.sha}` }); - const hasNextRevision = !!(await repository.getNextCommit(rightCommit.sha, rightFileUri.path.slice(1))); + const rightFileUri = router.buildUri({ ref: rightCommit.sha }, leftFileUri); + const hasNextRevision = !!(await repository.getNextCommit(rightCommit.sha, rightFileUri.path)); const query = queryString.stringify({ base: leftFileUri.with({ query: '' }).toString(), diff --git a/extensions/github1s/src/commands/global.ts b/extensions/github1s/src/commands/global.ts index 25c106553..72448990a 100644 --- a/extensions/github1s/src/commands/global.ts +++ b/extensions/github1s/src/commands/global.ts @@ -10,8 +10,8 @@ import { getRecentRepositories, removeRecentRepository } from '@/helpers/context import { adapterManager } from '@/adapters'; export const commandOpenOnOfficialPage = async () => { - const location = (await router.getHistory()).location; - const routerParser = await router.resolveParser(); + const location = router.getHistory().location; + const routerParser = router.getParser(); const fullPath = `${location.pathname}${location.search}${location.hash}`; const externalLink = await routerParser.buildExternalLink(fullPath); @@ -60,7 +60,7 @@ export const commandOpenRepository = async () => { const choice = quickPick.activeItems[0]; const repository = choice === manualInputItem ? quickPick.value : choice.label; const targetLink = vscode.Uri.parse((await router.href()) || '').with({ - path: await (await router.resolveParser()).buildTreePath(repository), + path: await router.getParser().buildTreePath(repository), }); vscode.commands.executeCommand('vscode.open', targetLink); quickPick.hide(); diff --git a/extensions/github1s/src/commands/ref.ts b/extensions/github1s/src/commands/ref.ts index 08b68cee0..4d160cab4 100644 --- a/extensions/github1s/src/commands/ref.ts +++ b/extensions/github1s/src/commands/ref.ts @@ -20,8 +20,8 @@ const checkoutToItem: vscode.QuickPickItem = { // check out to branch/tag/commit const commandCheckoutTo = async () => { - const routerParser = await router.resolveParser(); - const routeState = await router.getState(); + const routerParser = router.getParser(); + const routeState = router.getState(); const quickPick = vscode.window.createQuickPick(); const loadMoreRefPickerItems = async () => { diff --git a/extensions/github1s/src/extension.ts b/extensions/github1s/src/extension.ts index adc332673..15e742514 100644 --- a/extensions/github1s/src/extension.ts +++ b/extensions/github1s/src/extension.ts @@ -49,15 +49,11 @@ export async function activate(context: vscode.ExtensionContext) { // initialize the VSCode's state according to the router url const initialVSCodeState = async () => { - const routerState = await router.getState(); - const scheme = adapterManager.getCurrentScheme(); + const routerState = router.getState(); - if (routerState.pageType === PageType.Tree && routerState.filePath) { - vscode.commands.executeCommand( - 'revealInExplorer', - vscode.Uri.parse('').with({ scheme, path: `/${routerState.filePath}` }), - ); - } else if (routerState.pageType === PageType.Blob && routerState.filePath) { + if (routerState.pageType === PageType.Tree && routerState.filePath !== '/') { + vscode.commands.executeCommand('revealInExplorer', router.buildUri({ path: routerState.filePath })); + } else if (routerState.pageType === PageType.Blob && routerState.filePath !== '/') { const { startLine, endLine } = routerState; let documentShowOptions: vscode.TextDocumentShowOptions = {}; if (startLine || endLine) { @@ -65,10 +61,7 @@ const initialVSCodeState = async () => { const endPosition = new vscode.Position((endLine || startLine)! - 1, 1 << 20); documentShowOptions = { selection: new vscode.Range(startPosition, endPosition) }; } - vscode.window.showTextDocument( - vscode.Uri.parse('').with({ scheme, path: `/${routerState.filePath}` }), - documentShowOptions, - ); + vscode.window.showTextDocument(router.buildUri({ path: routerState.filePath }), documentShowOptions); } else if (routerState.pageType === PageType.CodeReviewList) { vscode.commands.executeCommand('github1s.views.codeReviewList.focus'); } else if (routerState.pageType === PageType.CommitList) { diff --git a/extensions/github1s/src/helpers/submodule.ts b/extensions/github1s/src/helpers/submodule.ts index 91e5c5f00..0ac890b40 100644 --- a/extensions/github1s/src/helpers/submodule.ts +++ b/extensions/github1s/src/helpers/submodule.ts @@ -3,6 +3,7 @@ * @author netcon */ +import { AdapterManager } from '@/adapters/manager'; import { FileSystemError, Uri } from 'vscode'; // the code below is come from https://github.com/microsoft/vscode/blob/1.52.1/extensions/git/src/git.ts#L661 @@ -73,7 +74,7 @@ export const parseGitmodules = (raw: string): Submodule[] => { return result; }; -export const parseSubmoduleUrl = (url: string) => { +export const parseSubmoduleUrl = async (url: string) => { try { let host = ''; let path = ''; @@ -87,20 +88,19 @@ export const parseSubmoduleUrl = (url: string) => { host = submoduleUri.authority; path = submoduleUri.path; } - let submoduleScheme = 'github1s'; + let subScheme = 'github1s'; if (/\bgithub\.com/i.test(host)) { - submoduleScheme = 'github1s'; + subScheme = 'github1s'; } else if (/\bgitlab\.com/i.test(host)) { - submoduleScheme = 'gitlab1s'; + subScheme = 'gitlab1s'; } else if (/\bbitbucket\.org/i.test(host)) { - submoduleScheme = 'bitbucket1s'; + subScheme = 'bitbucket1s'; } else { throw FileSystemError.Unavailable('only github submodules are supported now'); } - const [submoduleOwner, submoduleRepoPart] = path.split('/').filter(Boolean); - // if there are a repo which the name endsWith '.git' (likes conwnet/demo.git), this ambiguity may cause a problem - const submoduleRepo = submoduleRepoPart.endsWith('.git') ? submoduleRepoPart.slice(0, -4) : submoduleRepoPart; - return [submoduleScheme, `${submoduleOwner}/${submoduleRepo}`]; + + const subRepoPath = path.endsWith('.git') ? path.slice(0, -4) : path; + return [subScheme, subRepoPath.split('/').filter(Boolean).join('/')]; } catch (e) { throw FileSystemError.Unavailable('Can not found valid submodule declare'); } diff --git a/extensions/github1s/src/helpers/util.ts b/extensions/github1s/src/helpers/util.ts index a1a15c29e..bf491fca9 100644 --- a/extensions/github1s/src/helpers/util.ts +++ b/extensions/github1s/src/helpers/util.ts @@ -5,6 +5,7 @@ export const noop = () => {}; export const isNil = (value: any) => value === undefined || value === null; +export const isString = (value: any) => typeof value === 'string'; export const trimStart = (str: string, chars: string = ' '): string => { let index = 0; @@ -32,11 +33,19 @@ export const joinPath = (...segments: string[]): string => { }); }; +export const normalizePath = (path: string): string => (path.startsWith('/') ? path : `/${path}`); + +export const concatPath = (basePath: string, path: string): string => { + return joinPath(normalizePath(basePath), path); +}; + export const dirname = (path: string): string => { const trimmedPath = trimEnd(path, '/'); return trimmedPath.substr(0, trimmedPath.lastIndexOf('/')) || ''; }; +export const getFileTreeItemDescription = (path: string): string | boolean => dirname(path) || false; + export const basename = (path: string): string => { const trimmedPath = trimEnd(path, '/'); return trimmedPath.substr(trimmedPath.lastIndexOf('/') + 1) || ''; @@ -56,10 +65,3 @@ export const prop = (obj: object, path: (string | number)[] = []): any => { export const last = (array: readonly T[]): T => { return array[array.length - 1]; }; - -export const encodeFilePath = (filePath: string): string => { - return filePath - .split('/') - .map((segment) => encodeURIComponent(segment)) - .join('/'); -}; diff --git a/extensions/github1s/src/listeners/router/explorer.ts b/extensions/github1s/src/listeners/router/explorer.ts index b129f5c29..082e53433 100644 --- a/extensions/github1s/src/listeners/router/explorer.ts +++ b/extensions/github1s/src/listeners/router/explorer.ts @@ -42,6 +42,6 @@ export const explorerRouterListener = (currentState: RouterState, previousState: GitHub1sChangedFileDecorationProvider.getInstance().updateDecorations(); GitHub1sSubmoduleDecorationProvider.getInstance().updateDecorations(); GitHub1sSourceControlDecorationProvider.getInstance().updateDecorations(); - GitHub1sFileSearchProvider.getInstance().loadFilesForCurrentAuthority(); + GitHub1sFileSearchProvider.getInstance().loadFilesForCurrentWorkspace(); } }; diff --git a/extensions/github1s/src/listeners/vscode.ts b/extensions/github1s/src/listeners/vscode.ts index 6f96d4a5f..d0c867e85 100644 --- a/extensions/github1s/src/listeners/vscode.ts +++ b/extensions/github1s/src/listeners/vscode.ts @@ -13,9 +13,9 @@ import { adapterManager } from '@/adapters'; const handleRouterOnActiveEditorChange = async (editor: vscode.TextEditor | undefined) => { // replace current url when user change active editor - const { repo, ref, pageType } = await router.getState(); + const { repo, ref, pageType } = router.getState(); const activeFileUri = editor?.document.uri; - const routerParser = await router.resolveParser(); + const routerParser = router.getParser(); // only `tree/blob` page will replace url with the active editor change if (![PageType.Tree, PageType.Blob].includes(pageType)) { @@ -32,7 +32,7 @@ const handleRouterOnActiveEditorChange = async (editor: vscode.TextEditor | unde return; } - const browserPath = await routerParser.buildBlobPath(repo, ref, activeFileUri.path.slice(1)); + const browserPath = await routerParser.buildBlobPath(repo, ref, activeFileUri.path); router.replace(browserPath); }; @@ -50,8 +50,8 @@ const handlegutterBlameOpenContextOnActiveEditorChange = async () => { // add the line number anchor when user selection lines in a editor const handleRouterOnTextEditorSelectionChange = async (editor: vscode.TextEditor) => { - const { repo, ref, pageType } = await router.getState(); - const routerParser = await router.resolveParser(); + const { repo, ref, pageType } = router.getState(); + const routerParser = router.getParser(); // only add the line number anchor when pageType is PageType.Blob if (pageType !== PageType.Blob || !editor?.selection) { @@ -62,12 +62,12 @@ const handleRouterOnTextEditorSelectionChange = async (editor: vscode.TextEditor const browserPath = await routerParser.buildBlobPath( repo, ref, - activeFileUri.path.slice(1), + activeFileUri.path, !editor.selection.isEmpty ? editor.selection.start.line + 1 : undefined, editor.selection.end.line !== editor.selection.start.line ? editor.selection.end.line + 1 : undefined, ); - browserPath !== (await router.getPath()) && router.replace(browserPath); + browserPath !== router.getPath() && router.replace(browserPath); }; // refresh file history view if active editor changed diff --git a/extensions/github1s/src/messages.ts b/extensions/github1s/src/messages.ts index 00fc06b3d..8851abfa3 100644 --- a/extensions/github1s/src/messages.ts +++ b/extensions/github1s/src/messages.ts @@ -14,7 +14,7 @@ export const showSourcegraphSearchMessage = (() => { return; } alreadyShown = true; - const { repo, ref } = await router.getState(); + const { repo, ref } = router.getState(); const url = `https://sourcegraph.com/github.com/${repo}@${ref}`; vscode.window.showInformationMessage(`The code search ability is powered by [Sourcegraph](${url})`); }; diff --git a/extensions/github1s/src/providers/decorations/changed-file.ts b/extensions/github1s/src/providers/decorations/changed-file.ts index f9ee6da14..b7a8cedbe 100644 --- a/extensions/github1s/src/providers/decorations/changed-file.ts +++ b/extensions/github1s/src/providers/decorations/changed-file.ts @@ -43,7 +43,7 @@ export const changedFileDecorationDataMap: { [key: string]: FileDecoration } = { }; const getFileDecorationFromChangeFiles = (uri: Uri, changedFiles: ChangedFile[]): FileDecoration | null => { - const changedFile = changedFiles.find((changedFile) => changedFile.path === uri.path.slice(1)); + const changedFile = changedFiles.find((changedFile) => changedFile.path === uri.path); if (changedFile) { return changedFileDecorationDataMap[changedFile.status]; @@ -51,7 +51,7 @@ const getFileDecorationFromChangeFiles = (uri: Uri, changedFiles: ChangedFile[]) // we have to determine the changed folder manually rather then use // the `propagate` property of FileDecoration, because the file tree // in the file explorer is lazy load - const folderPath = `${uri.path.slice(1)}/`; + const folderPath = uri.path.endsWith('/') ? uri.path : `${uri.path}/`; const includeChangedFile = changedFiles.find((changedFile) => changedFile.path.startsWith(folderPath)); if (includeChangedFile) { return { @@ -63,15 +63,13 @@ const getFileDecorationFromChangeFiles = (uri: Uri, changedFiles: ChangedFile[]) }; const getFileDecorationForCodeReview = async (uri: Uri, codeReviewId: string): Promise => { - const [repo] = (uri.authority || (await router.getAuthority()))?.split('+') || []; - const repository = Repository.getInstance(uri.scheme, repo); + const repository = Repository.getInstanceByUri(uri); const changedFiles = await repository.getCodeReviewChangedFiles(codeReviewId); return getFileDecorationFromChangeFiles(uri, changedFiles); }; const getFileDecorationForCommit = async (uri: Uri, commitSha: string): Promise => { - const [repo] = (uri.authority || (await router.getAuthority()))?.split('+') || []; - const repository = Repository.getInstance(uri.scheme, repo); + const repository = Repository.getInstanceByUri(uri); const changedFiles = await repository.getCommitChangedFiles(commitSha); return getFileDecorationFromChangeFiles(uri, changedFiles); }; @@ -105,14 +103,13 @@ export class GitHub1sChangedFileDecorationProvider implements FileDecorationProv return null; } - return router.getState().then((routerState) => { - if (routerState.pageType === PageType.CodeReview) { - return getFileDecorationForCodeReview(uri, routerState.codeReviewId); - } - if (routerState.pageType === PageType.Commit) { - return getFileDecorationForCommit(uri, routerState.commitSha); - } - return null; - }); + const routerState = router.getState(); + if (routerState.pageType === PageType.CodeReview) { + return getFileDecorationForCodeReview(uri, routerState.codeReviewId); + } + if (routerState.pageType === PageType.Commit) { + return getFileDecorationForCommit(uri, routerState.commitSha); + } + return null; } } diff --git a/extensions/github1s/src/providers/decorations/source-control.ts b/extensions/github1s/src/providers/decorations/source-control.ts index 7f30b8371..0a7f352ad 100644 --- a/extensions/github1s/src/providers/decorations/source-control.ts +++ b/extensions/github1s/src/providers/decorations/source-control.ts @@ -56,17 +56,15 @@ export class GitHub1sSourceControlDecorationProvider implements FileDecorationPr } if (uri.scheme === GitHub1sSourceControlDecorationProvider.codeReviewSchema) { - return router.getState().then((routerState) => { - const query = queryString.parse(uri.query); - return +(routerState as any).codeReviewId === +query.id! ? selectedViewItemDecoration : null; - }); + const routerState = router.getState(); + const query = queryString.parse(uri.query); + return +(routerState as any).codeReviewId === +query.id! ? selectedViewItemDecoration : null; } if (uri.scheme === GitHub1sSourceControlDecorationProvider.commitSchema) { - return router.getState().then((routerState) => { - const query = queryString.parse(uri.query); - return (routerState as any).commitSha === query.sha ? selectedViewItemDecoration : null; - }); + const routerState = router.getState(); + const query = queryString.parse(uri.query); + return (routerState as any).commitSha === query.sha ? selectedViewItemDecoration : null; } } } diff --git a/extensions/github1s/src/providers/decorations/submodule.ts b/extensions/github1s/src/providers/decorations/submodule.ts index b24dbfd11..080852ad0 100644 --- a/extensions/github1s/src/providers/decorations/submodule.ts +++ b/extensions/github1s/src/providers/decorations/submodule.ts @@ -16,6 +16,7 @@ import { } from 'vscode'; import { GitHub1sFileSystemProvider } from '../file-system'; import { Directory } from '../file-system/types'; +import adapterManager from '@/adapters/manager'; export class GitHub1sSubmoduleDecorationProvider implements FileDecorationProvider, Disposable { private static instance: GitHub1sSubmoduleDecorationProvider | null = null; @@ -49,6 +50,9 @@ export class GitHub1sSubmoduleDecorationProvider implements FileDecorationProvid } provideFileDecoration(uri: Uri, _token: CancellationToken): ProviderResult { + if (!adapterManager.getAllAdapters().some((adapter) => adapter.scheme === uri.scheme)) { + return null; + } return GitHub1sFileSystemProvider.getInstance() .lookup(uri, false) .then((entry) => { diff --git a/extensions/github1s/src/providers/definition.ts b/extensions/github1s/src/providers/definition.ts index b11c92a9b..f78b027ce 100644 --- a/extensions/github1s/src/providers/definition.ts +++ b/extensions/github1s/src/providers/definition.ts @@ -8,6 +8,18 @@ import router from '@/router'; import { showSourcegraphSymbolMessage } from '@/messages'; import adapterManager from '@/adapters/manager'; +export const mapScopeScheme = (scopeScheme: string) => { + if (scopeScheme === 'github') { + return 'github1s'; + } else if (scopeScheme === 'gitlab') { + return 'gitlab1s'; + } else if (scopeScheme === 'bitbucket') { + return 'bitbucket1s'; + } else { + return scopeScheme; + } +}; + export class GitHub1sDefinitionProvider implements vscode.DefinitionProvider, vscode.Disposable { private static instance: GitHub1sDefinitionProvider | null = null; private readonly disposable: vscode.Disposable; @@ -37,12 +49,10 @@ export class GitHub1sDefinitionProvider implements vscode.DefinitionProvider, vs return []; } - const authority = document.uri.authority || (await router.getAuthority()); - const [repo, ref] = authority.split('+').filter(Boolean); - const { scheme, path } = document.uri; + const { scheme, repo, ref, path } = router.parseUri(document.uri); const { line, character } = position; - const dataSource = await adapterManager.getCurrentAdapter().resolveDataSource(); + const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); const symbolDefinitions = await dataSource.provideSymbolDefinitions(repo, ref, path, line, character, symbol); if (symbolDefinitions.length) { @@ -50,17 +60,14 @@ export class GitHub1sDefinitionProvider implements vscode.DefinitionProvider, vs } return symbolDefinitions.map(({ scope, path, range }) => { - const isSameRepo = !scope || (scope.scheme === scheme && scope.repo === repo); + const toScheme = mapScopeScheme(scope?.scheme || ''); + const isSameRepo = !scope || (toScheme === scheme && scope.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: `/${path}` }) - : vscode.Uri.parse('').with({ - scheme: scope!.scheme, - authority: `${scope!.repo}+${scope!.ref}`, - path: `/${path}`, - }); + ? document.uri.with({ path }) + : router.buildUri({ scheme: toScheme, repo: scope?.repo, ref: scope?.ref, path }); const { start, end } = range; return { uri, diff --git a/extensions/github1s/src/providers/file-search.ts b/extensions/github1s/src/providers/file-search.ts index d1b9f839f..15a5ca48c 100644 --- a/extensions/github1s/src/providers/file-search.ts +++ b/extensions/github1s/src/providers/file-search.ts @@ -30,7 +30,7 @@ export class GitHub1sFileSearchProvider implements FileSearchProvider, Disposabl // Once we have loaded the files, it will also populate the files into // fileSystemProvider's cache. So after that, we don't have to send // a request when you open the new directory in explorer late - this.loadFilesForCurrentAuthority(); + this.loadFilesForCurrentWorkspace(); } public static getInstance(): GitHub1sFileSearchProvider { @@ -44,27 +44,30 @@ export class GitHub1sFileSearchProvider implements FileSearchProvider, Disposabl this.disposable?.dispose(); } - // load the files for current authority - async loadFilesForCurrentAuthority() { - return this.getFileUris(await router.getAuthority()); + // load the files for current workspace + async loadFilesForCurrentWorkspace() { + return this.getFileUris(); } /** - * Get all files for the repo with specified by `authority`. + * Get all files for the repo with specified for current workspace. * The response of corresponding API maybe truncated, if so, * we should not insert the response to the fileSystemProvider's * cache, and the fuzzy search maybe not work fine */ - getFileUris = reuseable(async (authority: string): Promise => { - if (this.fileUrisMap.has(authority)) { - return this.fileUrisMap.get(authority)!; + getFileUris = reuseable(async (): Promise => { + const currentAdapter = adapterManager.getCurrentAdapter(); + const scheme = currentAdapter.scheme; + const { repo, ref } = router.getState(); + const cacheKey = `${scheme}:${repo}+${ref}`; + + if (this.fileUrisMap.has(cacheKey)) { + return this.fileUrisMap.get(cacheKey)!; } - const [repo, ref] = authority.split('+'); - const currentAdapter = adapterManager.getCurrentAdapter(); const dataSource = await currentAdapter.resolveDataSource(); - const rootDirectoryData = await dataSource.provideDirectory(repo, ref, '', true); - const rootDirectoryUri = Uri.parse('').with({ scheme: currentAdapter.scheme, authority, path: '/' }); + const rootDirectoryData = await dataSource.provideDirectory(repo, ref, '/', true); + const rootDirectoryUri = router.buildUri({ scheme, repo, ref, path: '/' }); // the number of items in the tree array maybe exceeded maximum limit, only // insert the data to fileSystemProvider's cache if `treeData.truncated` is false @@ -77,8 +80,8 @@ export class GitHub1sFileSearchProvider implements FileSearchProvider, Disposabl const fileUris = (rootDirectoryData?.entries || []) .filter((item) => item.type === adapterTypes.FileType.File) - .map((item) => Uri.joinPath(rootDirectoryUri, item.path)); - this.fileUrisMap.set(authority, fileUris); + .map((item) => rootDirectoryUri.with({ path: item.path })); + this.fileUrisMap.set(cacheKey, fileUris); return fileUris; }); @@ -87,8 +90,8 @@ export class GitHub1sFileSearchProvider implements FileSearchProvider, Disposabl _options: FileSearchOptions, _token: CancellationToken, ): ProviderResult { - return router.getAuthority().then(async (authority) => { - return matchSorter(await this.getFileUris(authority), query.pattern); + return new Promise(async (resolve) => { + resolve(matchSorter(await this.getFileUris(), query.pattern)); }); } } diff --git a/extensions/github1s/src/providers/file-system/index.ts b/extensions/github1s/src/providers/file-system/index.ts index 1bbcb1b3e..1ef1c2a97 100644 --- a/extensions/github1s/src/providers/file-system/index.ts +++ b/extensions/github1s/src/providers/file-system/index.ts @@ -64,7 +64,7 @@ export class GitHub1sFileSystemProvider implements FileSystemProvider, Disposabl // insert DirectoryEntry into the cache `this.root` public async populateWithDirectoryEntities(base: Uri, entries: adapterTypes.DirectoryEntry[]) { - const baseDirectory = await this.lookupAsDirectory(base, true); + const baseDirectory = await this.lookupAsDirectory(base.with({ path: '/' }), true); if (!baseDirectory) { return; } @@ -86,17 +86,16 @@ export class GitHub1sFileSystemProvider implements FileSystemProvider, Disposabl } // --- lookup - // ensure the authority field in `the uri of returned entry` is exists public async lookup(uri: Uri, silent: false): Promise; public async lookup(uri: Uri, silent: boolean): Promise; public async lookup(uri: Uri, silent: boolean): Promise { const parts = uri.path.split('/').filter(Boolean); - // if the authority of uri is empty, we should use `current authority` - const authority = uri.authority || (await router.getAuthority()); - if (!this.root.has(authority)) { - this.root.set(authority, createEntry(adapterTypes.FileType.Directory, uri.with({ authority, path: '/' }), '')); + const { scheme, repo, ref } = router.parseUri(uri); + const lookupKey = `${scheme}:${repo}+${ref}`; + if (!this.root.has(lookupKey)) { + this.root.set(lookupKey, createEntry(adapterTypes.FileType.Directory, uri.with({ path: '/' }), '')); } - let entry = this.root.get(authority); + let entry = this.root.get(lookupKey); for (const part of parts) { let child: Entry | undefined; if (entry instanceof Directory) { @@ -147,38 +146,17 @@ export class GitHub1sFileSystemProvider implements FileSystemProvider, Disposabl return this.lookup(uri, false); } - // it used by `@/src/providers/fileDecorationProvider.ts` - // update the uri of a git submodule as directory, which the type of corresponding githubEntry should be `commit`. - // the `directory.uri.authority` and the `directory.uri.path` must belong to the `parent repository` before called. - // and the `directory.name` is the corresponding `directory name` in `parent repository` before called. - // once the function is called successful, the `directory.uri.authority` field, the `directory.uri.path`, - // and the `directory.uri.name` field would be changed to the `submodule repository's`. - // - // so this function could be called only once for a submodule directory, for example: - // - the directory argument before called may looks like: - // { - // uri: { - // scheme: 'github1s', - // authority: 'conwnet+github1s+master', // this is the authority of `parent repository` - // path: '/some/submodule/path' // the corresponding path in `parent repository` - // }, - // name: 'vscode', // the name is the `directory name` of `parent repository` before called - // entries: null, // the entries should be null to indicated we haven't call this for `parent` - // isSubmodule: true, // this Directory must be a submodule - // ...otherFields - // } - // - and the directory argument after called may looks like: - // { - // uri: { - // scheme: 'github1s', - // authority: 'microsoft+vscode+master', // this is the authority of `submodule repository` - // path: '/' // the `path` filed should be '/' to indicated to the root directory of `submodule repository` - // }, - // name: '', // the name is the '' to indicated it is a root directory of `submodule repository` - // entries: Map {...}, // the entries contains the files of `submodule repository` - // isSubmodule: true, // this Directory must be a submodule - // ...otherFields - // } + /** + * Prepares a submodule directory for loading from its own repository. + * + * Before the first read, `directory.uri` and `directory.name` locate the submodule in its parent repository. + * The parent URI may have an empty authority when it belongs to the current workspace. This method resolves + * the matching `.gitmodules` entry, then changes the directory to represent the submodule repository root: + * `directory.uri` receives an explicit repository and ref, and `directory.name` becomes empty. The same + * directory is also registered in `root` under the submodule repository key. + * + * This method does not populate `directory.entries`; `readDirectory` does that after the repository switch. + */ private _updateSubmoduleDirectory = reuseable(async (directory: Directory): Promise<[string, FileType][]> => { // if the directory is not submodule, or it has be called already if (!directory.isSubmodule || directory.entries) { @@ -197,17 +175,14 @@ export class GitHub1sFileSystemProvider implements FileSystemProvider, Disposabl if (!gitmoduleData) { throw FileSystemError.FileNotFound(`can't found corresponding declare in .gitmodules`); } - const [submoduleScheme, submoduleRepo] = parseSubmoduleUrl(gitmoduleData.url); - const submoduleAuthority = `${submoduleRepo}+${directory.sha || 'HEAD'}`; + const subRef = directory.sha || 'HEAD'; + const [subScheme, subRepo] = await parseSubmoduleUrl(gitmoduleData.url); + const lookupKey = `${subScheme}:${subRepo}+${subRef}`; directory.name = ''; // update the name field to '' to indicated it is an root directory // update the uri field to indicated it is belong the `submodule repository` - directory.uri = Uri.parse('').with({ - scheme: submoduleScheme, - authority: submoduleAuthority, - path: '/', - }); + directory.uri = router.buildUri({ scheme: subScheme, repo: subRepo, ref: subRef, path: '/' }); // insert the directory in to this.root map because it indicated another repository - this.root.set(submoduleAuthority, directory); + this.root.set(lookupKey, directory); return []; }); @@ -224,11 +199,11 @@ export class GitHub1sFileSystemProvider implements FileSystemProvider, Disposabl if (parent.isSubmodule) { await this._updateSubmoduleDirectory(parent); } - const [repo, ref] = parent.uri.authority.split('+'); - const path = Uri.joinPath(parent.uri, parent.name).path.slice(1); // delete leading '/' - const dataSource = await this._resolveDataSource(uri.scheme); + const { scheme, repo, ref } = router.parseUri(parent.uri); + const path = Uri.joinPath(parent.uri, parent.name).path; + const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); const data = await dataSource.provideDirectory(repo, ref, path, false); - data?.entries && (await this.populateWithDirectoryEntities(uri, data.entries)); + data?.entries && (await this.populateWithDirectoryEntities(parent.uri, data.entries)); return parent.getNameTypePairs(); }, (uri) => uri.toString(), @@ -236,20 +211,15 @@ export class GitHub1sFileSystemProvider implements FileSystemProvider, Disposabl readFile = reuseable( async (uri: Uri): Promise => { - let { scheme, authority, path } = uri; - // if `authority` is same with current, try to find it with `this.lookupAsFile`, - // we can't use `router.getAuthority()` directly because this file may be in submodule - if (authority === workspace.workspaceFolders?.[0].uri.authority) { - const file = (await this.lookupAsFile(uri, false))!; - scheme = file.uri.scheme; - authority = file.uri.authority; - path = joinPath(file.uri.path, file.name); - } - const cacheKey = `${scheme} ${authority} ${path}`; + // If a file belongs to the current workspace, + // check its existence to avoid unnecessary content requests. + // It is efficient for some built-in files like `.vscode/...` + !uri.authority && (await this.lookupAsFile(uri, false)); + const { scheme, repo, ref, path } = router.parseUri(uri); + const cacheKey = `${scheme}:${repo}+${ref}${path}`; if (!this.contentCache.has(cacheKey)) { - const [repo, ref] = authority.split('+'); - const dataSource = await this._resolveDataSource(scheme); - const data = await dataSource.provideFile(repo, ref, path.slice(1)); + const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); + const data = await dataSource.provideFile(repo, ref, path); data && this.contentCache.set(cacheKey, data.content); } return this.contentCache.get(cacheKey) || new Uint8Array(); diff --git a/extensions/github1s/src/providers/hover.ts b/extensions/github1s/src/providers/hover.ts index 06d3150ef..4ef875b74 100644 --- a/extensions/github1s/src/providers/hover.ts +++ b/extensions/github1s/src/providers/hover.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import router from '@/router'; import { getSourcegraphUrl } from '@/helpers/urls'; import { adapterManager } from '@/adapters'; +import { mapScopeScheme } from './definition'; const getSemanticMarkdownSuffix = (sourcegraphUrl: string) => ` @@ -45,11 +46,10 @@ export class GitHub1sHoverProvider implements vscode.HoverProvider, vscode.Dispo symbol: string, ): Promise { const { line, character } = position; - const authority = document.uri.authority || (await router.getAuthority()); - const [repo, ref] = authority.split('+').filter(Boolean); - const dataSource = await adapterManager.getCurrentAdapter().resolveDataSource(); + const { scheme, repo, ref, path } = router.parseUri(document.uri); + const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); - const requestParams = [repo, ref, document.uri.path, line, character, symbol] as const; + const requestParams = [repo, ref, path, line, character, symbol] as const; const definitions = await dataSource.provideSymbolDefinitions(...requestParams); if (!definitions.length) { @@ -58,16 +58,13 @@ export class GitHub1sHoverProvider implements vscode.HoverProvider, vscode.Dispo // use the information of first definition as hover context const target = definitions[0]; - const isSameRepo = !target.scope || (target.scope.scheme === document.uri.scheme && target.scope.repo === repo); + const toScheme = mapScopeScheme(target.scope?.scheme || ''); + const isSameRepo = !target.scope || (toScheme === document.uri.scheme && target.scope.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: target.scope?.scheme, - authority: `${target.scope?.repo}+${target.scope?.ref}`, - path: `/${target.path}`, - }); + ? document.uri.with({ path: target.path }) + : router.buildUri({ scheme: toScheme, repo: target.scope?.repo, ref: target.scope?.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 @@ -91,9 +88,7 @@ export class GitHub1sHoverProvider implements vscode.HoverProvider, vscode.Dispo return null; } - const authority = document.uri.authority || (await router.getAuthority()); - const [repo, ref] = authority.split('+').filter(Boolean); - const path = document.uri.path; + const { scheme, repo, ref, path } = router.parseUri(document.uri); const { line, character } = position; // get the sourcegraph url for current symbol @@ -104,7 +99,7 @@ export class GitHub1sHoverProvider implements vscode.HoverProvider, vscode.Dispo const searchBasedMardownPromise = this.getSearchBasedHover(document, position, symbol); // get the hover result based on sourcegraph lsif - const dataSource = await adapterManager.getCurrentAdapter().resolveDataSource(); + const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); const symbolHover = await dataSource.provideSymbolHover(...requestParams); const markdown = symbolHover ? symbolHover.markdown : await searchBasedMardownPromise; diff --git a/extensions/github1s/src/providers/index.ts b/extensions/github1s/src/providers/index.ts index a67ccf303..9da617be5 100644 --- a/extensions/github1s/src/providers/index.ts +++ b/extensions/github1s/src/providers/index.ts @@ -15,11 +15,10 @@ import { GitHub1sSourceControlDecorationProvider } from './decorations/source-co import { GitHub1sDefinitionProvider } from './definition'; import { GitHub1sReferenceProvider } from './reference'; import { GitHub1sHoverProvider } from './hover'; +import router from '@/router'; export const EMPTY_FILE_SCHEME = 'github1s-empty-file'; -export const emptyFileUri = vscode.Uri.parse('').with({ - scheme: EMPTY_FILE_SCHEME, -}); +export const emptyFileUri = vscode.Uri.from({ scheme: EMPTY_FILE_SCHEME }); export const registerVSCodeProviders = () => { const context = getExtensionContext(); diff --git a/extensions/github1s/src/providers/reference.ts b/extensions/github1s/src/providers/reference.ts index 1ef0b5824..9339d5244 100644 --- a/extensions/github1s/src/providers/reference.ts +++ b/extensions/github1s/src/providers/reference.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import router from '@/router'; import { showSourcegraphSymbolMessage } from '@/messages'; import adapterManager from '@/adapters/manager'; +import { mapScopeScheme } from './definition'; export class GitHub1sReferenceProvider implements vscode.ReferenceProvider, vscode.Disposable { private static instance: GitHub1sReferenceProvider | null = null; @@ -38,12 +39,10 @@ export class GitHub1sReferenceProvider implements vscode.ReferenceProvider, vsco return []; } - const authority = document.uri.authority || (await router.getAuthority()); - const [repo, ref] = authority.split('+').filter(Boolean); - const { scheme, path } = document.uri; + const { scheme, repo, ref, path } = router.parseUri(document.uri); const { line, character } = position; - const dataSource = await adapterManager.getCurrentAdapter().resolveDataSource(); + const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); const symbolReferences = await dataSource.provideSymbolReferences(repo, ref, path, line, character, symbol); if (symbolReferences.length) { @@ -51,17 +50,14 @@ export class GitHub1sReferenceProvider implements vscode.ReferenceProvider, vsco } return symbolReferences.map(({ scope, path, range }) => { - const isSameRepo = !scope || (scope.scheme === scheme && scope.repo === repo); + const toScheme = mapScopeScheme(scope?.scheme || ''); + const isSameRepo = !scope || (toScheme === scheme && scope.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: `/${path}` }) - : vscode.Uri.parse('').with({ - scheme: scope!.scheme, - authority: `${scope!.repo}+${scope!.ref}`, - path: `/${path}`, - }); + ? document.uri.with({ path }) + : router.buildUri({ scheme: toScheme, repo: scope?.repo, ref: scope?.ref, path }); const { start, end } = range; return { uri, diff --git a/extensions/github1s/src/providers/text-search.ts b/extensions/github1s/src/providers/text-search.ts index 12b0e3584..7898664d2 100644 --- a/extensions/github1s/src/providers/text-search.ts +++ b/extensions/github1s/src/providers/text-search.ts @@ -36,18 +36,17 @@ export class GitHub1sTextSearchProvider implements vscode.TextSearchProvider, vs progress: vscode.Progress, _token: vscode.CancellationToken, ) { - return router.getAuthority().then(async (authority) => { - const [repo, ref] = authority.split('+'); - const dataSource = await adapterManager.getCurrentAdapter().resolveDataSource(); + return Promise.resolve().then(async () => { + const { scheme, repo, ref } = router.getState(); + const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); const searchOptions = { page: 1, pageSize: 100, includes: options.includes, excludes: options.excludes }; const searchResults = await dataSource.provideTextSearchResults(repo, ref, query, searchOptions); - const currentScheme = adapterManager.getCurrentScheme(); (searchResults.results || []).forEach((item) => { // because we set the authority of workspace as '' (on application start) // at src/vs/code/browser/workbench/workbench.ts // so don't specified authority here, or the VS Code won't use the results - const fileUri = vscode.Uri.parse('').with({ scheme: currentScheme, path: `/${item.path}` }); + const fileUri = router.buildUri({ path: item.path }); const ranges = ensureArray(item.ranges).map((range) => createVscodeRange(range)); const previewMatches = ensureArray(item.preview.matches).map((match) => createVscodeRange(match)); const preview = { text: item.preview.text, matches: previewMatches }; diff --git a/extensions/github1s/src/repository/commit-manager.ts b/extensions/github1s/src/repository/commit-manager.ts index 02685e413..ef87e8ad5 100644 --- a/extensions/github1s/src/repository/commit-manager.ts +++ b/extensions/github1s/src/repository/commit-manager.ts @@ -160,8 +160,8 @@ export class CommitManager { if (this._currentPage === 1 && commits.length) { this._latestCommitSha = commits[0].sha; - // also map `this._from` to the first commit if currentPage is 1 and filePath is empty - !this._filePath && CommitManager._commitMap.set(this._from, commits[0]); + // also map `this._from` to the first commit for repository history + this._filePath === '/' && CommitManager._commitMap.set(this._from, commits[0]); } commits.forEach((commit) => { CommitManager._commitMap.set(commit.sha, commit); diff --git a/extensions/github1s/src/repository/index.ts b/extensions/github1s/src/repository/index.ts index 541bf149a..c43109f0c 100644 --- a/extensions/github1s/src/repository/index.ts +++ b/extensions/github1s/src/repository/index.ts @@ -3,11 +3,13 @@ * @author netcon */ +import * as vscode from 'vscode'; import { adapterManager } from '@/adapters'; import { CommitManager } from './commit-manager'; import { CodeReviewManager } from './code-review-manager'; import { BranchTagManager } from './branch-tag-manager'; import { BlameRange } from '@/adapters/types'; +import router from '@/router'; export class Repository { private static instanceMap = new Map(); @@ -24,6 +26,16 @@ export class Repository { return Repository.instanceMap.get(mapKey)!; } + public static getInstanceByUri(uri: vscode.Uri) { + const { scheme, repo } = router.parseUri(uri); + return Repository.getInstance(scheme, repo); + } + + public static getCurrentInstance() { + const routerState = router.getState(); + return Repository.getInstance(routerState.scheme, routerState.repo); + } + private constructor( private _scheme: string, private _repo: string, @@ -65,32 +77,32 @@ export class Repository { return this._branchTagManager.hasMoreTags(...args); } - getCommitList(ref: string = 'HEAD', filePath: string = '', forceUpdate: boolean = false) { + getCommitList(ref: string = 'HEAD', filePath: string = '/', forceUpdate: boolean = false) { return CommitManager.getInstance(this._scheme, this._repo, ref, filePath).getList(forceUpdate); } getCommitItem(ref: string, forceUpdate: boolean = false) { - return CommitManager.getInstance(this._scheme, this._repo, ref, '').getItem(forceUpdate); + return CommitManager.getInstance(this._scheme, this._repo, ref, '/').getItem(forceUpdate); } - loadMoreCommits(ref: string = 'HEAD', filePath: string = '') { + loadMoreCommits(ref: string = 'HEAD', filePath: string = '/') { return CommitManager.getInstance(this._scheme, this._repo, ref, filePath).loadMore(); } - hasMoreCommits(ref: string = 'HEAD', filePath: string = '') { + hasMoreCommits(ref: string = 'HEAD', filePath: string = '/') { return CommitManager.getInstance(this._scheme, this._repo, ref, filePath).hasMore(); } getCommitChangedFiles(ref: string, forceUpdate: boolean = false) { - return CommitManager.getInstance(this._scheme, this._repo, ref, '').getChangedFiles(forceUpdate); + return CommitManager.getInstance(this._scheme, this._repo, ref, '/').getChangedFiles(forceUpdate); } loadMoreCommitChangedFiles(ref: string) { - return CommitManager.getInstance(this._scheme, this._repo, ref, '').loadMoreChangedFiles(); + return CommitManager.getInstance(this._scheme, this._repo, ref, '/').loadMoreChangedFiles(); } hasMoreCommitChangedFiles(ref: string) { - return CommitManager.getInstance(this._scheme, this._repo, ref, '').hasMoreChangedFiles(); + return CommitManager.getInstance(this._scheme, this._repo, ref, '/').hasMoreChangedFiles(); } getFileLatestCommit(ref: string, filePath: string) { diff --git a/extensions/github1s/src/router/index.ts b/extensions/github1s/src/router/index.ts index 43b3e390b..6fa564b1c 100644 --- a/extensions/github1s/src/router/index.ts +++ b/extensions/github1s/src/router/index.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { History, createMemoryHistory, parsePath, Action } from 'history'; -import { RouterParser, RouterState } from '@/adapters/types'; +import { Adapter, RouterParser, RouterState } from '@/adapters/types'; import { Barrier } from '@/helpers/async'; import adapterManager from '@/adapters/manager'; import { EventEmitter } from './events'; @@ -16,14 +16,20 @@ export interface UrlManager { replace: (url: string) => void | Promise; } +export interface UriState { + scheme: string; + repo: string; + ref: string; + path: string; +} + export class Router extends EventEmitter { private static instance: Router; private _state: RouterState | null = null; private _history: History | null = null; + private _adapter: Adapter | null = null; private _parser: RouterParser | null = null; - // ensure router has been initialized - private _barrier: Barrier = new Barrier(); private _manager: UrlManager | null = null; public static getInstance() { @@ -34,72 +40,88 @@ export class Router extends EventEmitter { } // initialize the router with current url in browser + // must be called before any other method is called async initialize(urlManager: UrlManager) { this._manager = urlManager; - this._parser = await adapterManager.getCurrentAdapter().resolveRouterParser(); + this._adapter = adapterManager.getCurrentAdapter(); const { path: pathname, query, fragment } = vscode.Uri.parse(await this._manager.href()); const path = pathname + (query ? `?${query}` : '') + (fragment ? `#${fragment}` : ''); + this._parser = await this._adapter.resolveRouterParser(); this._state = await this._parser.parsePath(path); this._history = createMemoryHistory({ initialEntries: [path] }); this._history.listen(async ({ action, location }) => { const prevState = this._state; const targetPath = `${location.pathname}${location.search}${location.hash}`; - const routerParser = await adapterManager.getCurrentAdapter().resolveRouterParser(); this._manager?.[action === Action.Push ? 'push' : 'replace'](targetPath); - this._state = await routerParser.parsePath(targetPath); + this._state = await this._parser!.parsePath(targetPath); super.notifyListeners(this._state, prevState); }); - this._barrier.open(); } // get the routerState for current url - public async getState(): Promise { - await this._barrier.wait(); - return this._state!; - } - - // compute the file URI authority of current routerState - public async getAuthority(): Promise { - const state = await this.getState(); - return `${state.repo}+${state.ref}`; + public getState(): RouterState & { scheme: string } { + return { ...this._state!, scheme: this._adapter!.scheme }; } - public async getHistory() { - await this._barrier.wait(); + public getHistory() { return this._history!; } - public async getPath() { - await this._barrier.wait(); + public getPath() { const { pathname, search, hash } = this._history!.location; return `${pathname}${search}${hash}`; } // push the url with current history - public async push(path: string) { - await this._barrier.wait(); + public push(path: string) { const emptyState = { pathname: '', search: '', hash: '' }; return this._history!.push({ ...emptyState, ...parsePath(encodeURI(path)) }); } // replace the url with current history - public async replace(path: string) { - await this._barrier.wait(); + public replace(path: string) { const emptyState = { pathname: '', search: '', hash: '' }; return this._history!.replace({ ...emptyState, ...parsePath(encodeURI(path)) }); } - public async resolveParser(): Promise { - await this._barrier.wait(); + public getParser(): RouterParser { return this._parser!; } public async href(): Promise { return this._manager?.href(); } + + public parseUri(uri: vscode.Uri): UriState { + const scheme = uri.scheme; + const [repo, ref] = uri.authority ? uri.authority.split('+') : [this._state!.repo, this._state!.ref]; + return { scheme, repo, ref, path: uri.path || '/' }; + } + + public buildUri(state?: Partial, base?: vscode.Uri): vscode.Uri { + const mergedState: Parameters['with']>[0] = {}; + + if (state?.hasOwnProperty('scheme')) { + mergedState.scheme = state.scheme || ''; + } + if (state && state.repo && !state.ref) { + throw new Error('ref is required when repo is provided'); + } + if (state?.hasOwnProperty('ref')) { + const repo = state.repo || base?.authority.split('+')[0] || this._state!.repo; + mergedState.authority = repo && state.ref ? `${repo}+${state.ref}` : ''; + } + if (state?.hasOwnProperty('path')) { + mergedState.path = `/${state.path?.split('/').filter(Boolean).join('/') || ''}`; + } + + return base + ? base.with(mergedState) + : vscode.Uri.from({ scheme: adapterManager.getCurrentScheme(), path: '/', ...mergedState }); + } } export default Router.getInstance(); diff --git a/extensions/github1s/src/statusbar/checkout.ts b/extensions/github1s/src/statusbar/checkout.ts index 2ef40f49f..0125e102f 100644 --- a/extensions/github1s/src/statusbar/checkout.ts +++ b/extensions/github1s/src/statusbar/checkout.ts @@ -10,7 +10,7 @@ export const updateCheckoutTo = (() => { const checkoutItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); const refreshItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 90); return async () => { - const { repo, ref } = await router.getState(); + const { repo, ref } = router.getState(); checkoutItem.text = `$(git-branch) ${ref}`; checkoutItem.tooltip = 'Checkout branch/tag/commit...'; diff --git a/extensions/github1s/src/statusbar/sponsors.ts b/extensions/github1s/src/statusbar/sponsors.ts index 6ec592f25..18cd0480d 100644 --- a/extensions/github1s/src/statusbar/sponsors.ts +++ b/extensions/github1s/src/statusbar/sponsors.ts @@ -9,7 +9,7 @@ import { adapterManager } from '@/adapters'; import { PlatformName } from '@/adapters/types'; const resolveSourcegraphLink = async () => { - const { repo, ref } = await router.getState(); + const { repo, ref } = router.getState(); switch (adapterManager.getCurrentAdapter().platformName) { case PlatformName.GitHub: return `https://sourcegraph.com/github.com/${repo}@${ref}`; diff --git a/extensions/github1s/src/views/code-review-list.ts b/extensions/github1s/src/views/code-review-list.ts index 40d79aea6..61c9f7957 100644 --- a/extensions/github1s/src/views/code-review-list.ts +++ b/extensions/github1s/src/views/code-review-list.ts @@ -13,6 +13,7 @@ import adapterManager from '@/adapters/manager'; import * as adapterTypes from '@/adapters/types'; import { getChangedFileDiffCommand, getCodeReviewChangedFiles } from '@/changes/files'; import { GitHub1sSourceControlDecorationProvider } from '@/providers/decorations/source-control'; +import { getFileTreeItemDescription } from '@/helpers/util'; enum CodeReviewState { OPEN = 'open', @@ -115,7 +116,7 @@ export class CodeReviewTreeDataProvider implements vscode.TreeDataProvider { this._loadingBarrier && (await this._loadingBarrier.wait()); const currentScheme = adapterManager.getCurrentScheme(); - const { repo } = await router.getState(); + const { repo } = router.getState(); const repository = Repository.getInstance(currentScheme, repo); const codeReviews = await repository.getCodeReviewList(this._forceUpdate); const codeReviewTreeItems = codeReviews.map((codeReview) => { @@ -152,7 +153,7 @@ export class CodeReviewTreeDataProvider implements vscode.TreeDataProvider { this._loadingBarrier && (await this._loadingBarrier.wait()); - const scheme = adapterManager.getCurrentScheme(); - const { repo } = await router.getState(); - const repository = Repository.getInstance(scheme, repo); + const repository = Repository.getCurrentInstance(); const _codeReview = await repository.getCodeReviewItem(codeReview.id); const changedFiles = _codeReview ? await getCodeReviewChangedFiles(_codeReview) : []; const changedFileItems = changedFiles.map((changedFile) => { @@ -179,7 +178,7 @@ export class CodeReviewTreeDataProvider implements vscode.TreeDataProvider { const shortCommitSha = commit.sha.slice(0, 7); @@ -61,7 +62,7 @@ export class CommitTreeDataProvider implements vscode.TreeDataProvider { @@ -113,7 +114,7 @@ export class CommitTreeDataProvider implements vscode.TreeDataProvider; From 71505a77212b83c90a9cf5e820a41797fd379b82 Mon Sep 17 00:00:00 2001 From: netcon Date: Tue, 11 Aug 2026 20:59:20 +0800 Subject: [PATCH 7/9] chore: optimize import codes (#715) * feat: simplify repository import * feat: simplify adapter import * feat: standardize router state * chore: fix ci * fix: submodule read file --- .github/workflows/build.yml | 2 +- .github/workflows/test-wtih-vscode-build.yml | 13 ++++++++-- extensions/github1s/src/adapters/index.ts | 10 +++++-- extensions/github1s/src/adapters/manager.ts | 10 +++---- extensions/github1s/src/changes/files.ts | 10 +++---- extensions/github1s/src/changes/quick-diff.ts | 6 ++--- extensions/github1s/src/commands/blame.ts | 4 +-- .../github1s/src/commands/code-review.ts | 8 +++--- extensions/github1s/src/commands/commit.ts | 7 +++-- extensions/github1s/src/commands/editor.ts | 13 +++++----- extensions/github1s/src/commands/global.ts | 7 +++-- extensions/github1s/src/commands/ref.ts | 7 ++--- extensions/github1s/src/extension.ts | 4 +-- extensions/github1s/src/helpers/submodule.ts | 1 - extensions/github1s/src/listeners/vscode.ts | 4 +-- .../src/providers/decorations/changed-file.ts | 12 +++++---- .../src/providers/decorations/submodule.ts | 4 +-- .../github1s/src/providers/definition.ts | 4 +-- .../github1s/src/providers/file-search.ts | 10 +++---- .../src/providers/file-system/index.ts | 19 +++++++------- extensions/github1s/src/providers/hover.ts | 6 ++--- extensions/github1s/src/providers/index.ts | 5 ++-- .../github1s/src/providers/reference.ts | 4 +-- .../github1s/src/providers/text-search.ts | 6 ++--- .../src/repository/branch-tag-manager.ts | 10 +++---- .../src/repository/code-review-manager.ts | 8 +++--- .../github1s/src/repository/commit-manager.ts | 8 +++--- extensions/github1s/src/repository/index.ts | 15 +++-------- extensions/github1s/src/router/index.ts | 17 +++++------- extensions/github1s/src/statusbar/sponsors.ts | 4 +-- .../github1s/src/views/code-review-list.ts | 20 +++++--------- extensions/github1s/src/views/commit-list.ts | 26 +++++++------------ extensions/github1s/src/views/index.ts | 4 +-- 33 files changed, 131 insertions(+), 157 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc265cde4..ed83fe6ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,4 +29,4 @@ jobs: - run: npm run eslint - run: npm run build - uses: microsoft/playwright-github-action@v1 - - run: GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} npm run test:ci + - run: npm run test:ci diff --git a/.github/workflows/test-wtih-vscode-build.yml b/.github/workflows/test-wtih-vscode-build.yml index cc4fc5409..d04876a72 100644 --- a/.github/workflows/test-wtih-vscode-build.yml +++ b/.github/workflows/test-wtih-vscode-build.yml @@ -10,6 +10,9 @@ on: jobs: build-with-vscode-build: + permissions: + contents: read + strategy: matrix: os: [macos-14] @@ -19,6 +22,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: @@ -26,7 +31,11 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm install && cd vscode-web && npm install - - run: cd vscode-web && npm run build + - name: Build VS Code web + working-directory: vscode-web + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npm run build - run: npm run link && npm run build - uses: microsoft/playwright-github-action@v1 - - run: GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} npm run test:ci + - run: npm run test:ci diff --git a/extensions/github1s/src/adapters/index.ts b/extensions/github1s/src/adapters/index.ts index 4a80be861..87fbe9425 100644 --- a/extensions/github1s/src/adapters/index.ts +++ b/extensions/github1s/src/adapters/index.ts @@ -9,7 +9,7 @@ import { GitLab1sAdapter } from './gitlab1s'; import { BitbucketAdapter } from './bitbucket1s'; import { Npmjs1sAdapter } from './npmjs1s'; import { OSSInsightAdapter } from './ossinsight'; -import { DataSource, PlatformName, RouterParser } from './types'; +import { Adapter, DataSource, PlatformName, RouterParser } from './types'; const emptyAdapter = { scheme: 'empty', @@ -29,4 +29,10 @@ export const registerAdapters = async (): Promise => { ]); }; -export { adapterManager }; +export const getAdapter = (scheme?: string): Adapter => { + return adapterManager.getAdapter(scheme); +}; + +export const getAllAdapters = (): Adapter[] => { + return adapterManager.getAllAdapters(); +}; diff --git a/extensions/github1s/src/adapters/manager.ts b/extensions/github1s/src/adapters/manager.ts index 5bb38c430..82a91a060 100644 --- a/extensions/github1s/src/adapters/manager.ts +++ b/extensions/github1s/src/adapters/manager.ts @@ -40,21 +40,17 @@ export class AdapterManager { return Array.from(this.adaptersMap.values()); } - public getAdapter(scheme: string): Adapter { + public getAdapter(scheme?: string): Adapter { + scheme = scheme || this.getCurrentScheme(); if (!this.adaptersMap.has(scheme)) { throw new Error(`Adapter with scheme '${scheme}' can not found.`); } return this.adaptersMap.get(scheme)!; } - public getCurrentScheme(): string { + private getCurrentScheme(): string { return vscode.workspace.workspaceFolders?.[0]?.uri?.scheme || 'empty'; } - - public getCurrentAdapter(): Adapter { - const scheme = this.getCurrentScheme(); - return this.getAdapter(scheme); - } } export default AdapterManager.getInstance(); diff --git a/extensions/github1s/src/changes/files.ts b/extensions/github1s/src/changes/files.ts index 8d4b0ddd7..8ff463660 100644 --- a/extensions/github1s/src/changes/files.ts +++ b/extensions/github1s/src/changes/files.ts @@ -6,7 +6,6 @@ import * as vscode from 'vscode'; import * as queryString from 'query-string'; import * as adapterTypes from '@/adapters/types'; -import adapterManager from '@/adapters/manager'; import router from '@/router'; import { basename } from '@/helpers/util'; import { emptyFileUri } from '@/providers'; @@ -22,10 +21,9 @@ interface VSCodeChangedFile { export const getCodeReviewChangedFiles = async ( codeReview: adapterTypes.CodeReview & { sourceSha: string; targetSha: string }, ) => { + const repository = Repository.getCurrentInstance(); const baseRootUri = router.buildUri({ ref: codeReview.targetSha }); const headRootUri = router.buildUri({ ref: codeReview.sourceSha }, baseRootUri); - - const repository = Repository.getCurrentInstance(); const changedFiles = await repository.getCodeReviewChangedFiles(codeReview.id); return changedFiles.map((changedFile) => { @@ -42,14 +40,13 @@ export const getCodeReviewChangedFiles = async ( }; export const getCommitChangedFiles = async (commit: adapterTypes.Commit) => { + const repository = Repository.getCurrentInstance(); // if the commit.parents is more than one element // the parents[1].sha should be the merge source commitSha // so we use the parents[0].sha as the parent commitSha const parentCommitSha = commit?.parents?.[0] || ''; const baseRootUri = router.buildUri({ ref: parentCommitSha }); const headRootUri = router.buildUri({ ref: commit.sha }, baseRootUri); - - const repository = Repository.getCurrentInstance(); const changedFiles = await repository.getCommitChangedFiles(commit.sha); return changedFiles.map((commitFile) => { @@ -67,16 +64,15 @@ export const getCommitChangedFiles = async (commit: adapterTypes.Commit) => { export const getChangedFiles = async (): Promise => { const routerState = router.getState(); + const repository = Repository.getCurrentInstance(); // code review page if (routerState.pageType === adapterTypes.PageType.CodeReview) { - const repository = Repository.getInstance(routerState.scheme, routerState.repo); const codeReview = await repository.getCodeReviewItem(routerState.codeReviewId); return codeReview ? getCodeReviewChangedFiles(codeReview) : []; } // commit page else if (routerState.pageType === adapterTypes.PageType.Commit) { - const repository = Repository.getInstance(routerState.scheme, routerState.repo); const commit = await repository.getCommitItem(routerState.commitSha); return commit ? getCommitChangedFiles(commit) : []; } diff --git a/extensions/github1s/src/changes/quick-diff.ts b/extensions/github1s/src/changes/quick-diff.ts index d620b218c..a2b2afc88 100644 --- a/extensions/github1s/src/changes/quick-diff.ts +++ b/extensions/github1s/src/changes/quick-diff.ts @@ -5,14 +5,14 @@ import * as vscode from 'vscode'; import router from '@/router'; +import { getAdapter } from '@/adapters'; import { Repository } from '@/repository'; import { emptyFileUri } from '@/providers'; -import adapterManager from '@/adapters/manager'; import * as adapterTypes from '@/adapters/types'; // get the original source uri when the `routerState.pageType` is `PageType.PULL` const getOriginalResourceForPull = async (uri: vscode.Uri, codeReviewId: string): Promise => { - const repository = await Repository.getCurrentInstance(); + const repository = Repository.getCurrentInstance(); const codeReviewFiles = await repository.getCodeReviewChangedFiles(codeReviewId); const changedFile = codeReviewFiles?.find((changedFile) => changedFile.path === uri.path); @@ -65,7 +65,7 @@ export class GitHub1sQuickDiffProvider implements vscode.QuickDiffProvider { provideOriginalResource(uri: vscode.Uri, _token: vscode.CancellationToken): vscode.ProviderResult { const routerState = router.getState(); // only the file belong to current workspace could be provided a quick diff - if (uri.scheme !== routerState.scheme || uri.authority) { + if (uri.scheme !== getAdapter().scheme || uri.authority) { return null; } diff --git a/extensions/github1s/src/commands/blame.ts b/extensions/github1s/src/commands/blame.ts index 837a94997..967228a3e 100644 --- a/extensions/github1s/src/commands/blame.ts +++ b/extensions/github1s/src/commands/blame.ts @@ -8,9 +8,9 @@ import { relativeTimeTo } from '@/helpers/date'; import { last } from '@/helpers/util'; import { setVSCodeContext } from '@/helpers/vscode'; import router from '@/router'; +import { getAdapter } from '@/adapters'; import { Repository } from '@/repository'; import { BlameRange, PlatformName } from '@/adapters/types'; -import { adapterManager } from '@/adapters'; const ageColors = [ '#f66a0a', @@ -204,7 +204,7 @@ class EditorGitBlame { this.refreshDisposables.forEach((disposable) => disposable.dispose()); setVSCodeContext('github1s:features:gutterBlame:open', true); const { scheme } = router.parseUri(this.editor.document.uri); - const { platformName } = adapterManager.getAdapter(scheme); + const platformName = getAdapter(scheme).platformName; (await this.getBlameRanges()).forEach((blameRange) => { const hoverMessage = createCommitMessagePreviewMarkdown(blameRange, platformName); diff --git a/extensions/github1s/src/commands/code-review.ts b/extensions/github1s/src/commands/code-review.ts index 8c943d312..8664749d6 100644 --- a/extensions/github1s/src/commands/code-review.ts +++ b/extensions/github1s/src/commands/code-review.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import router from '@/router'; +import { getAdapter } from '@/adapters'; import { CodeReviewTreeItem, getCodeReviewTreeItemLabel, @@ -12,7 +13,6 @@ import { } from '@/views/code-review-list'; import { codeReviewRequestTreeDataProvider } from '@/views'; import { CodeReviewType } from '@/adapters/types'; -import { adapterManager } from '@/adapters'; import { Repository } from '@/repository'; const CodeReviewTypeName = { @@ -23,7 +23,7 @@ const CodeReviewTypeName = { }; const checkCodeReviewExists = async (repo: string, codeReviewId: string) => { - const adapter = adapterManager.getCurrentAdapter(); + const adapter = getAdapter(); const dataSoruce = await adapter.resolveDataSource(); try { return !!(await dataSoruce.provideCodeReview(repo, codeReviewId)); @@ -44,10 +44,10 @@ const commandSwitchToCodeReview = async (codeReviewItemOrId?: string | CodeRevie ? codeReviewItemOrId : codeReviewItemOrId.codeReview.id : ''; + const adapter = getAdapter(); const { repo } = router.getState(); - const adapter = adapterManager.getCurrentAdapter(); - const typeName = CodeReviewTypeName[adapter.codeReviewType || CodeReviewType.CodeReview]; const repository = Repository.getCurrentInstance(); + const typeName = CodeReviewTypeName[adapter.codeReviewType || CodeReviewType.CodeReview]; // if the a codeReviewId isn't provided, use quickInput if (!codeReviewId) { diff --git a/extensions/github1s/src/commands/commit.ts b/extensions/github1s/src/commands/commit.ts index d7fa85d42..97e3df402 100644 --- a/extensions/github1s/src/commands/commit.ts +++ b/extensions/github1s/src/commands/commit.ts @@ -5,14 +5,13 @@ import * as vscode from 'vscode'; import router from '@/router'; +import { getAdapter } from '@/adapters'; +import { Repository } from '@/repository'; import { CommitTreeItem, getCommitTreeItemDescription } from '@/views/commit-list'; import { commitTreeDataProvider, fileHistoryTreeDataProvider } from '@/views'; -import { adapterManager } from '@/adapters'; -import { Repository } from '@/repository'; export const checkCommitExists = async (repo: string, commitSha: string) => { - const adapter = adapterManager.getCurrentAdapter(); - const dataSoruce = await adapter.resolveDataSource(); + const dataSoruce = await getAdapter().resolveDataSource(); try { return !!(await dataSoruce.provideCommit(repo, commitSha)); } catch (error) { diff --git a/extensions/github1s/src/commands/editor.ts b/extensions/github1s/src/commands/editor.ts index 24db76644..2eeeb9d94 100644 --- a/extensions/github1s/src/commands/editor.ts +++ b/extensions/github1s/src/commands/editor.ts @@ -54,8 +54,8 @@ const commandDiffViewOpenRightFile = async (fileUri: vscode.Uri) => { // get the file uri with the concrete commit sha, the `ref` in // `fileUri.authority` maybe newer but not related this file const getConcreteFileUri = async (fileUri: vscode.Uri) => { - const { ref, path } = router.parseUri(fileUri); - const repository = Repository.getInstanceByUri(fileUri); + const { scheme, repo, ref, path } = router.parseUri(fileUri); + const repository = Repository.getInstance(scheme, repo); const commit = await repository.getFileLatestCommit(ref, path); const latestCommitSha = commit?.sha || (await repository.getCommitItem(ref))?.sha; @@ -70,9 +70,8 @@ const commandOpenFilePreviousRevision = async (fileUri: vscode.Uri) => { // a normal file editor (not a diff editor), just use `fileUri` in this case queryBaseUriStr ? vscode.Uri.parse(queryBaseUriStr as string) : fileUri, ); - const { repo, ref: rightCommitSha } = router.parseUri(rightFileUri); - - const repository = Repository.getInstanceByUri(rightFileUri); + const { scheme, repo, ref: rightCommitSha } = router.parseUri(rightFileUri); + const repository = Repository.getInstance(scheme, repo); const leftCommit = await repository.getPreviousCommit(rightCommitSha, rightFileUri.path); // if we can't find previous commit, use the `emptyFileUri` as the leftFileUri const leftFileUri = leftCommit ? router.buildUri({ ref: leftCommit.sha }, rightFileUri) : emptyFileUri; @@ -101,8 +100,8 @@ const commandOpenFilePreviousRevision = async (fileUri: vscode.Uri) => { const commandOpenFileNextRevision = async (fileUri: vscode.Uri) => { const leftFileUri = await getConcreteFileUri(fileUri); - const { ref: leftCommitSha } = router.parseUri(leftFileUri); - const repository = Repository.getInstanceByUri(leftFileUri); + const { scheme, repo, ref: leftCommitSha } = router.parseUri(leftFileUri); + const repository = Repository.getInstance(scheme, repo); const rightCommit = await repository.getNextCommit(leftCommitSha, leftFileUri.path); if (!rightCommit) { diff --git a/extensions/github1s/src/commands/global.ts b/extensions/github1s/src/commands/global.ts index 72448990a..ccaa79cdc 100644 --- a/extensions/github1s/src/commands/global.ts +++ b/extensions/github1s/src/commands/global.ts @@ -5,9 +5,9 @@ import * as vscode from 'vscode'; import router from '@/router'; +import { getAdapter } from '@/adapters'; import { relativeTimeTo } from '@/helpers/date'; import { getRecentRepositories, removeRecentRepository } from '@/helpers/context'; -import { adapterManager } from '@/adapters'; export const commandOpenOnOfficialPage = async () => { const location = router.getHistory().location; @@ -68,14 +68,13 @@ export const commandOpenRepository = async () => { }; const commandOpenOnlineEditor = async () => { - const currentScheme = adapterManager.getCurrentScheme(); - const onlineEditorPath = ['github1s', 'ossinsight'].includes(currentScheme) ? '/editor' : '/'; + const onlineEditorPath = ['github1s', 'ossinsight'].includes(getAdapter().scheme) ? '/editor' : '/'; const targetLink = vscode.Uri.parse((await router.href()) || '').with({ path: onlineEditorPath }); return vscode.commands.executeCommand('vscode.open', targetLink); }; const commandRefreshRepository = async () => { - if (['github1s', 'gitlab1s'].includes(adapterManager.getCurrentScheme())) { + if (['github1s', 'gitlab1s'].includes(getAdapter().scheme)) { await vscode.commands.executeCommand('github1s.commands.syncSourcegraphRepository'); } vscode.commands.executeCommand('workbench.action.reloadWindow'); diff --git a/extensions/github1s/src/commands/ref.ts b/extensions/github1s/src/commands/ref.ts index 4d160cab4..fa1174a43 100644 --- a/extensions/github1s/src/commands/ref.ts +++ b/extensions/github1s/src/commands/ref.ts @@ -5,7 +5,6 @@ import * as vscode from 'vscode'; import router from '@/router'; -import { adapterManager } from '@/adapters'; import { Repository } from '@/repository'; const loadMorePickerItem: vscode.QuickPickItem = { @@ -20,14 +19,12 @@ const checkoutToItem: vscode.QuickPickItem = { // check out to branch/tag/commit const commandCheckoutTo = async () => { - const routerParser = router.getParser(); const routeState = router.getState(); + const repository = Repository.getCurrentInstance(); const quickPick = vscode.window.createQuickPick(); const loadMoreRefPickerItems = async () => { quickPick.busy = true; - const scheme = adapterManager.getCurrentScheme(); - const repository = Repository.getInstance(scheme, routeState.repo); await Promise.all([repository.loadMoreBranches(), repository.loadMoreTags()]); const [branchRefs, tagRefs] = await Promise.all([repository.getBranchList(), repository.getTagList()]); const refPickerItems = [...branchRefs, ...tagRefs].map((ref) => ({ @@ -51,7 +48,7 @@ const commandCheckoutTo = async () => { } const selectedRef = choice === checkoutToItem ? quickPick.value : choice?.label; const targetRef = selectedRef.toUpperCase() !== 'HEAD' ? selectedRef : undefined; - router.push(await routerParser.buildTreePath(routeState.repo, targetRef)); + router.push(await router.getParser().buildTreePath(routeState.repo, targetRef)); quickPick.hide(); }); }; diff --git a/extensions/github1s/src/extension.ts b/extensions/github1s/src/extension.ts index 15e742514..7be17e063 100644 --- a/extensions/github1s/src/extension.ts +++ b/extensions/github1s/src/extension.ts @@ -5,14 +5,14 @@ import router from '@/router'; import * as vscode from 'vscode'; -import { PageType } from './adapters/types'; +import { PageType } from '@/adapters/types'; +import { registerAdapters } from '@/adapters'; import { registerCustomViews } from '@/views'; import { decorateStatusBar } from '@/statusbar'; import { registerEventListeners } from '@/listeners'; import { registerVSCodeProviders } from '@/providers'; import { registerGitHub1sCommands } from '@/commands'; import { updateSourceControlChanges } from '@/changes'; -import { adapterManager, registerAdapters } from '@/adapters'; import { addRecentRepositories, setExtensionContext } from '@/helpers/context'; const browserUrlManager = { diff --git a/extensions/github1s/src/helpers/submodule.ts b/extensions/github1s/src/helpers/submodule.ts index 0ac890b40..8b6167937 100644 --- a/extensions/github1s/src/helpers/submodule.ts +++ b/extensions/github1s/src/helpers/submodule.ts @@ -3,7 +3,6 @@ * @author netcon */ -import { AdapterManager } from '@/adapters/manager'; import { FileSystemError, Uri } from 'vscode'; // the code below is come from https://github.com/microsoft/vscode/blob/1.52.1/extensions/git/src/git.ts#L661 diff --git a/extensions/github1s/src/listeners/vscode.ts b/extensions/github1s/src/listeners/vscode.ts index d0c867e85..9fbb311bf 100644 --- a/extensions/github1s/src/listeners/vscode.ts +++ b/extensions/github1s/src/listeners/vscode.ts @@ -5,11 +5,11 @@ import * as vscode from 'vscode'; import router from '@/router'; +import { getAdapter } from '@/adapters'; import { setVSCodeContext } from '@/helpers/vscode'; import { getChangedFileFromSourceControl } from '@/commands/editor'; import { debounce } from '@/helpers/func'; import { PageType } from '@/adapters/types'; -import { adapterManager } from '@/adapters'; const handleRouterOnActiveEditorChange = async (editor: vscode.TextEditor | undefined) => { // replace current url when user change active editor @@ -24,7 +24,7 @@ const handleRouterOnActiveEditorChange = async (editor: vscode.TextEditor | unde // if the file which not belong to current workspace is opened, or no file // is opened, only retain `repo` (and `ref` if need) in browser url - if (!activeFileUri || activeFileUri?.authority || activeFileUri?.scheme !== adapterManager.getCurrentScheme()) { + if (!activeFileUri || activeFileUri?.authority || activeFileUri?.scheme !== getAdapter().scheme) { const browserPath = await (ref.toUpperCase() === 'HEAD' ? routerParser.buildTreePath(repo) : routerParser.buildTreePath(repo, ref)); diff --git a/extensions/github1s/src/providers/decorations/changed-file.ts b/extensions/github1s/src/providers/decorations/changed-file.ts index b7a8cedbe..b891bb063 100644 --- a/extensions/github1s/src/providers/decorations/changed-file.ts +++ b/extensions/github1s/src/providers/decorations/changed-file.ts @@ -15,9 +15,9 @@ import { ThemeColor, } from 'vscode'; import router from '@/router'; -import { ChangedFile, FileChangeStatus, PageType } from '@/adapters/types'; -import { adapterManager } from '@/adapters'; +import { getAdapter } from '@/adapters'; import { Repository } from '@/repository'; +import { ChangedFile, FileChangeStatus, PageType } from '@/adapters/types'; export const changedFileDecorationDataMap: { [key: string]: FileDecoration } = { [FileChangeStatus.Added]: { @@ -63,13 +63,15 @@ const getFileDecorationFromChangeFiles = (uri: Uri, changedFiles: ChangedFile[]) }; const getFileDecorationForCodeReview = async (uri: Uri, codeReviewId: string): Promise => { - const repository = Repository.getInstanceByUri(uri); + const { scheme, repo } = router.parseUri(uri); + const repository = Repository.getInstance(scheme, repo); const changedFiles = await repository.getCodeReviewChangedFiles(codeReviewId); return getFileDecorationFromChangeFiles(uri, changedFiles); }; const getFileDecorationForCommit = async (uri: Uri, commitSha: string): Promise => { - const repository = Repository.getInstanceByUri(uri); + const { scheme, repo } = router.parseUri(uri); + const repository = Repository.getInstance(scheme, repo); const changedFiles = await repository.getCommitChangedFiles(commitSha); return getFileDecorationFromChangeFiles(uri, changedFiles); }; @@ -99,7 +101,7 @@ export class GitHub1sChangedFileDecorationProvider implements FileDecorationProv } provideFileDecoration(uri: Uri, _token: CancellationToken): ProviderResult { - if (uri.scheme !== adapterManager.getCurrentScheme()) { + if (uri.scheme !== getAdapter().scheme) { return null; } diff --git a/extensions/github1s/src/providers/decorations/submodule.ts b/extensions/github1s/src/providers/decorations/submodule.ts index 080852ad0..7940fe4f5 100644 --- a/extensions/github1s/src/providers/decorations/submodule.ts +++ b/extensions/github1s/src/providers/decorations/submodule.ts @@ -14,9 +14,9 @@ import { ThemeColor, Uri, } from 'vscode'; +import { getAllAdapters } from '@/adapters'; import { GitHub1sFileSystemProvider } from '../file-system'; import { Directory } from '../file-system/types'; -import adapterManager from '@/adapters/manager'; export class GitHub1sSubmoduleDecorationProvider implements FileDecorationProvider, Disposable { private static instance: GitHub1sSubmoduleDecorationProvider | null = null; @@ -50,7 +50,7 @@ export class GitHub1sSubmoduleDecorationProvider implements FileDecorationProvid } provideFileDecoration(uri: Uri, _token: CancellationToken): ProviderResult { - if (!adapterManager.getAllAdapters().some((adapter) => adapter.scheme === uri.scheme)) { + if (!getAllAdapters().some((adapter) => adapter.scheme === uri.scheme)) { return null; } return GitHub1sFileSystemProvider.getInstance() diff --git a/extensions/github1s/src/providers/definition.ts b/extensions/github1s/src/providers/definition.ts index f78b027ce..3123f085e 100644 --- a/extensions/github1s/src/providers/definition.ts +++ b/extensions/github1s/src/providers/definition.ts @@ -5,8 +5,8 @@ import * as vscode from 'vscode'; import router from '@/router'; +import { getAdapter } from '@/adapters'; import { showSourcegraphSymbolMessage } from '@/messages'; -import adapterManager from '@/adapters/manager'; export const mapScopeScheme = (scopeScheme: string) => { if (scopeScheme === 'github') { @@ -52,7 +52,7 @@ export class GitHub1sDefinitionProvider implements vscode.DefinitionProvider, vs const { scheme, repo, ref, path } = router.parseUri(document.uri); const { line, character } = position; - const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); + const dataSource = await getAdapter(scheme).resolveDataSource(); const symbolDefinitions = await dataSource.provideSymbolDefinitions(repo, ref, path, line, character, symbol); if (symbolDefinitions.length) { diff --git a/extensions/github1s/src/providers/file-search.ts b/extensions/github1s/src/providers/file-search.ts index 15a5ca48c..fd3ae75fa 100644 --- a/extensions/github1s/src/providers/file-search.ts +++ b/extensions/github1s/src/providers/file-search.ts @@ -13,12 +13,12 @@ import { Uri, window, } from 'vscode'; +import { getAdapter } from '@/adapters'; import { matchSorter } from 'match-sorter'; import { reuseable } from '@/helpers/func'; import router from '@/router'; import * as adapterTypes from '@/adapters/types'; import { GitHub1sFileSystemProvider } from './file-system'; -import adapterManager from '@/adapters/manager'; export class GitHub1sFileSearchProvider implements FileSearchProvider, Disposable { private static instance: GitHub1sFileSearchProvider | null = null; @@ -56,18 +56,16 @@ export class GitHub1sFileSearchProvider implements FileSearchProvider, Disposabl * cache, and the fuzzy search maybe not work fine */ getFileUris = reuseable(async (): Promise => { - const currentAdapter = adapterManager.getCurrentAdapter(); - const scheme = currentAdapter.scheme; const { repo, ref } = router.getState(); - const cacheKey = `${scheme}:${repo}+${ref}`; + const cacheKey = `${repo}+${ref}`; if (this.fileUrisMap.has(cacheKey)) { return this.fileUrisMap.get(cacheKey)!; } - const dataSource = await currentAdapter.resolveDataSource(); + const dataSource = await getAdapter().resolveDataSource(); const rootDirectoryData = await dataSource.provideDirectory(repo, ref, '/', true); - const rootDirectoryUri = router.buildUri({ scheme, repo, ref, path: '/' }); + const rootDirectoryUri = router.buildUri({ repo, ref, path: '/' }); // the number of items in the tree array maybe exceeded maximum limit, only // insert the data to fileSystemProvider's cache if `treeData.truncated` is false diff --git a/extensions/github1s/src/providers/file-system/index.ts b/extensions/github1s/src/providers/file-system/index.ts index 1ef1c2a97..508dc2f24 100644 --- a/extensions/github1s/src/providers/file-system/index.ts +++ b/extensions/github1s/src/providers/file-system/index.ts @@ -13,12 +13,11 @@ import { FileStat, FileType, Uri, - workspace, } from 'vscode'; -import adapterManager from '@/adapters/manager'; +import { getAdapter } from '@/adapters'; import * as adapterTypes from '@/adapters/types'; import router from '@/router'; -import { noop, trimStart, basename, dirname, joinPath } from '@/helpers/util'; +import { noop, trimStart, basename, dirname } from '@/helpers/util'; import { parseGitmodules, parseSubmoduleUrl } from '@/helpers/submodule'; import { reuseable } from '@/helpers/func'; import { File, Directory, Entry } from './types'; @@ -58,10 +57,6 @@ export class GitHub1sFileSystemProvider implements FileSystemProvider, Disposabl this.disposable?.dispose(); } - private async _resolveDataSource(scheme: string) { - return adapterManager.getAdapter(scheme).resolveDataSource(); - } - // insert DirectoryEntry into the cache `this.root` public async populateWithDirectoryEntities(base: Uri, entries: adapterTypes.DirectoryEntry[]) { const baseDirectory = await this.lookupAsDirectory(base.with({ path: '/' }), true); @@ -201,7 +196,7 @@ export class GitHub1sFileSystemProvider implements FileSystemProvider, Disposabl } const { scheme, repo, ref } = router.parseUri(parent.uri); const path = Uri.joinPath(parent.uri, parent.name).path; - const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); + const dataSource = await getAdapter(scheme).resolveDataSource(); const data = await dataSource.provideDirectory(repo, ref, path, false); data?.entries && (await this.populateWithDirectoryEntities(parent.uri, data.entries)); return parent.getNameTypePairs(); @@ -214,11 +209,15 @@ export class GitHub1sFileSystemProvider implements FileSystemProvider, Disposabl // If a file belongs to the current workspace, // check its existence to avoid unnecessary content requests. // It is efficient for some built-in files like `.vscode/...` - !uri.authority && (await this.lookupAsFile(uri, false)); + // The uri is also reset to the correct one for the submodule. + if (!uri.authority) { + const file = (await this.lookupAsFile(uri, false))!; + uri = Uri.joinPath(file.uri, file.name); + } const { scheme, repo, ref, path } = router.parseUri(uri); const cacheKey = `${scheme}:${repo}+${ref}${path}`; if (!this.contentCache.has(cacheKey)) { - const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); + const dataSource = await getAdapter(scheme).resolveDataSource(); const data = await dataSource.provideFile(repo, ref, path); data && this.contentCache.set(cacheKey, data.content); } diff --git a/extensions/github1s/src/providers/hover.ts b/extensions/github1s/src/providers/hover.ts index 4ef875b74..38a1a9170 100644 --- a/extensions/github1s/src/providers/hover.ts +++ b/extensions/github1s/src/providers/hover.ts @@ -5,8 +5,8 @@ import * as vscode from 'vscode'; import router from '@/router'; +import { getAdapter } from '@/adapters'; import { getSourcegraphUrl } from '@/helpers/urls'; -import { adapterManager } from '@/adapters'; import { mapScopeScheme } from './definition'; const getSemanticMarkdownSuffix = (sourcegraphUrl: string) => ` @@ -47,7 +47,7 @@ export class GitHub1sHoverProvider implements vscode.HoverProvider, vscode.Dispo ): Promise { const { line, character } = position; const { scheme, repo, ref, path } = router.parseUri(document.uri); - const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); + const dataSource = await getAdapter(scheme).resolveDataSource(); const requestParams = [repo, ref, path, line, character, symbol] as const; const definitions = await dataSource.provideSymbolDefinitions(...requestParams); @@ -99,7 +99,7 @@ export class GitHub1sHoverProvider implements vscode.HoverProvider, vscode.Dispo const searchBasedMardownPromise = this.getSearchBasedHover(document, position, symbol); // get the hover result based on sourcegraph lsif - const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); + const dataSource = await getAdapter(scheme).resolveDataSource(); const symbolHover = await dataSource.provideSymbolHover(...requestParams); const markdown = symbolHover ? symbolHover.markdown : await searchBasedMardownPromise; diff --git a/extensions/github1s/src/providers/index.ts b/extensions/github1s/src/providers/index.ts index 9da617be5..331d8f521 100644 --- a/extensions/github1s/src/providers/index.ts +++ b/extensions/github1s/src/providers/index.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode'; -import adapterManager from '@/adapters/manager'; +import { getAllAdapters } from '@/adapters'; import { getExtensionContext } from '@/helpers/context'; import { GitHub1sFileSystemProvider } from './file-system'; import { GitHub1sFileSearchProvider } from './file-search'; @@ -22,8 +22,7 @@ export const emptyFileUri = vscode.Uri.from({ scheme: EMPTY_FILE_SCHEME }); export const registerVSCodeProviders = () => { const context = getExtensionContext(); - - const allSchemes = adapterManager.getAllAdapters().map((item) => item.scheme); + const allSchemes = getAllAdapters().map((item) => item.scheme); allSchemes.forEach((scheme) => { context.subscriptions.push( diff --git a/extensions/github1s/src/providers/reference.ts b/extensions/github1s/src/providers/reference.ts index 9339d5244..0a0b259c5 100644 --- a/extensions/github1s/src/providers/reference.ts +++ b/extensions/github1s/src/providers/reference.ts @@ -5,8 +5,8 @@ import * as vscode from 'vscode'; import router from '@/router'; +import { getAdapter } from '@/adapters'; import { showSourcegraphSymbolMessage } from '@/messages'; -import adapterManager from '@/adapters/manager'; import { mapScopeScheme } from './definition'; export class GitHub1sReferenceProvider implements vscode.ReferenceProvider, vscode.Disposable { @@ -42,7 +42,7 @@ export class GitHub1sReferenceProvider implements vscode.ReferenceProvider, vsco const { scheme, repo, ref, path } = router.parseUri(document.uri); const { line, character } = position; - const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); + const dataSource = await getAdapter(scheme).resolveDataSource(); const symbolReferences = await dataSource.provideSymbolReferences(repo, ref, path, line, character, symbol); if (symbolReferences.length) { diff --git a/extensions/github1s/src/providers/text-search.ts b/extensions/github1s/src/providers/text-search.ts index 7898664d2..455059546 100644 --- a/extensions/github1s/src/providers/text-search.ts +++ b/extensions/github1s/src/providers/text-search.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import router from '@/router'; -import adapterManager from '@/adapters/manager'; +import { getAdapter } from '@/adapters'; import { showSourcegraphSearchMessage } from '@/messages'; import * as adapterTypes from '@/adapters/types'; @@ -37,8 +37,8 @@ export class GitHub1sTextSearchProvider implements vscode.TextSearchProvider, vs _token: vscode.CancellationToken, ) { return Promise.resolve().then(async () => { - const { scheme, repo, ref } = router.getState(); - const dataSource = await adapterManager.getAdapter(scheme).resolveDataSource(); + const { repo, ref } = router.getState(); + const dataSource = await getAdapter().resolveDataSource(); const searchOptions = { page: 1, pageSize: 100, includes: options.includes, excludes: options.excludes }; const searchResults = await dataSource.provideTextSearchResults(repo, ref, query, searchOptions); diff --git a/extensions/github1s/src/repository/branch-tag-manager.ts b/extensions/github1s/src/repository/branch-tag-manager.ts index f1cbe22ed..d04bacf31 100644 --- a/extensions/github1s/src/repository/branch-tag-manager.ts +++ b/extensions/github1s/src/repository/branch-tag-manager.ts @@ -5,7 +5,7 @@ import { reuseable } from '@/helpers/func'; import { Branch, Tag } from '@/adapters/types'; -import { adapterManager } from '@/adapters'; +import { getAdapter } from '@/adapters'; export class BranchTagManager { private static instancesMap = new Map(); @@ -46,7 +46,7 @@ export class BranchTagManager { getBranchItem = reuseable(async (branchName: string, forceUpdate = false): Promise => { if (forceUpdate || !this._branchMap.has(branchName)) { - const dataSource = await adapterManager.getAdapter(this._scheme).resolveDataSource(); + const dataSource = await getAdapter(this._scheme).resolveDataSource(); const branch = await dataSource.provideBranch(this._repo, branchName); branch && this._branchMap.set(branchName, branch); } @@ -54,7 +54,7 @@ export class BranchTagManager { }); loadMoreBranches = reuseable(async (): Promise => { - const dataSource = await adapterManager.getAdapter(this._scheme).resolveDataSource(); + const dataSource = await getAdapter(this._scheme).resolveDataSource(); const queryOptions = { pageSize: this._branchPageSize, page: this._branchCurrentPage }; const branches = await dataSource.provideBranches(this._repo, queryOptions); @@ -81,7 +81,7 @@ export class BranchTagManager { getTagItem = reuseable(async (tagName: string, forceUpdate = false): Promise => { if (forceUpdate || !this._tagMap.has(tagName)) { - const dataSource = await adapterManager.getAdapter(this._scheme).resolveDataSource(); + const dataSource = await getAdapter(this._scheme).resolveDataSource(); const tag = await dataSource.provideTag(this._repo, tagName); tag && this._tagMap.set(tagName, tag); } @@ -89,7 +89,7 @@ export class BranchTagManager { }); loadMoreTags = reuseable(async (): Promise => { - const dataSource = await adapterManager.getAdapter(this._scheme).resolveDataSource(); + const dataSource = await getAdapter(this._scheme).resolveDataSource(); const queryOptions = { pageSize: this._tagPageSize, page: this._tagCurrentPage }; const tags = await dataSource.provideTags(this._repo, queryOptions); diff --git a/extensions/github1s/src/repository/code-review-manager.ts b/extensions/github1s/src/repository/code-review-manager.ts index 844df6705..36eddcf58 100644 --- a/extensions/github1s/src/repository/code-review-manager.ts +++ b/extensions/github1s/src/repository/code-review-manager.ts @@ -3,9 +3,9 @@ * @author netcon */ +import { getAdapter } from '@/adapters'; import { reuseable } from '@/helpers/func'; import { ChangedFile, CodeReview } from '@/adapters/types'; -import { adapterManager } from '@/adapters'; // manage changed files for a code review class CodeReviewChangedFilesManager { @@ -41,7 +41,7 @@ class CodeReviewChangedFilesManager { }); loadMore = reuseable(async (): Promise => { - const dataSource = await adapterManager.getAdapter(this._scheme).resolveDataSource(); + const dataSource = await getAdapter(this._scheme).resolveDataSource(); const changedFiles = await dataSource.provideCodeReviewChangedFiles(this._repo, this._codeReviewId, { pageSize: this._pageSize, page: this._currentPage, @@ -106,7 +106,7 @@ export class CodeReviewManager { !this._codeReviewMap.has(codeReviewId) || !isShaExists(this._codeReviewMap.get(codeReviewId)!) ) { - const dataSource = await adapterManager.getAdapter(this._scheme).resolveDataSource(); + const dataSource = await getAdapter(this._scheme).resolveDataSource(); const codeReview = await dataSource.provideCodeReview(this._repo, codeReviewId); codeReview && this._codeReviewMap.set(codeReviewId, codeReview); if (codeReview?.files) { @@ -121,7 +121,7 @@ export class CodeReviewManager { ); loadMore = reuseable(async (): Promise => { - const dataSource = await adapterManager.getAdapter(this._scheme).resolveDataSource(); + const dataSource = await getAdapter(this._scheme).resolveDataSource(); const queryOptions = { pageSize: this._pageSize, page: this._currentPage }; const codeReviews = await dataSource.provideCodeReviews(this._repo, queryOptions); diff --git a/extensions/github1s/src/repository/commit-manager.ts b/extensions/github1s/src/repository/commit-manager.ts index ef87e8ad5..951652a7c 100644 --- a/extensions/github1s/src/repository/commit-manager.ts +++ b/extensions/github1s/src/repository/commit-manager.ts @@ -3,9 +3,9 @@ * @author netcon */ +import { getAdapter } from '@/adapters'; import { reuseable } from '@/helpers/func'; import { ChangedFile, Commit } from '@/adapters/types'; -import { adapterManager } from '@/adapters'; // manage changed files for a commit class CommitChangedFilesManager { @@ -41,7 +41,7 @@ class CommitChangedFilesManager { }); loadMore = reuseable(async (): Promise => { - const dataSource = await adapterManager.getAdapter(this._scheme).resolveDataSource(); + const dataSource = await getAdapter(this._scheme).resolveDataSource(); const changedFiles = await dataSource.provideCommitChangedFiles(this._repo, this._commitSha, { pageSize: this._pageSize, page: this._currentPage, @@ -134,7 +134,7 @@ export class CommitManager { getItem = reuseable(async (forceUpdate: boolean = false): Promise => { if (forceUpdate || !CommitManager._commitMap.has(this._from)) { - const dataSource = await adapterManager.getAdapter(this._scheme).resolveDataSource(); + const dataSource = await getAdapter(this._scheme).resolveDataSource(); const commit = await dataSource.provideCommit(this._repo, this._from); commit && CommitManager._commitMap.set(this._from, commit); @@ -149,7 +149,7 @@ export class CommitManager { loadMore = reuseable(async (): Promise => { const commitList = this.resolveCommitList(); - const dataSource = await adapterManager.getAdapter(this._scheme).resolveDataSource(); + const dataSource = await getAdapter(this._scheme).resolveDataSource(); const queryOptions = { page: this._currentPage, pageSize: this._pageSize, diff --git a/extensions/github1s/src/repository/index.ts b/extensions/github1s/src/repository/index.ts index c43109f0c..2379cffa3 100644 --- a/extensions/github1s/src/repository/index.ts +++ b/extensions/github1s/src/repository/index.ts @@ -3,13 +3,12 @@ * @author netcon */ -import * as vscode from 'vscode'; -import { adapterManager } from '@/adapters'; +import router from '@/router'; +import { getAdapter } from '@/adapters'; import { CommitManager } from './commit-manager'; import { CodeReviewManager } from './code-review-manager'; import { BranchTagManager } from './branch-tag-manager'; import { BlameRange } from '@/adapters/types'; -import router from '@/router'; export class Repository { private static instanceMap = new Map(); @@ -26,14 +25,8 @@ export class Repository { return Repository.instanceMap.get(mapKey)!; } - public static getInstanceByUri(uri: vscode.Uri) { - const { scheme, repo } = router.parseUri(uri); - return Repository.getInstance(scheme, repo); - } - public static getCurrentInstance() { - const routerState = router.getState(); - return Repository.getInstance(routerState.scheme, routerState.repo); + return Repository.getInstance(getAdapter().scheme, router.getState().repo); } private constructor( @@ -148,7 +141,7 @@ export class Repository { async getFileBlameRanges(ref: string, path: string) { const cacheKey = `${ref} ${path}`; if (!this._blameRangesCache.has(cacheKey)) { - const dataSource = await adapterManager.getAdapter(this._scheme).resolveDataSource(); + const dataSource = await getAdapter(this._scheme).resolveDataSource(); const blameRanges = await dataSource.provideFileBlameRanges(this._repo, ref, path); this._blameRangesCache.set(cacheKey, blameRanges); } diff --git a/extensions/github1s/src/router/index.ts b/extensions/github1s/src/router/index.ts index 6fa564b1c..9062c360f 100644 --- a/extensions/github1s/src/router/index.ts +++ b/extensions/github1s/src/router/index.ts @@ -4,10 +4,9 @@ */ import * as vscode from 'vscode'; +import { getAdapter } from '@/adapters'; import { History, createMemoryHistory, parsePath, Action } from 'history'; -import { Adapter, RouterParser, RouterState } from '@/adapters/types'; -import { Barrier } from '@/helpers/async'; -import adapterManager from '@/adapters/manager'; +import { RouterParser, RouterState } from '@/adapters/types'; import { EventEmitter } from './events'; export interface UrlManager { @@ -28,7 +27,6 @@ export class Router extends EventEmitter { private _state: RouterState | null = null; private _history: History | null = null; - private _adapter: Adapter | null = null; private _parser: RouterParser | null = null; private _manager: UrlManager | null = null; @@ -43,11 +41,10 @@ export class Router extends EventEmitter { // must be called before any other method is called async initialize(urlManager: UrlManager) { this._manager = urlManager; - this._adapter = adapterManager.getCurrentAdapter(); const { path: pathname, query, fragment } = vscode.Uri.parse(await this._manager.href()); const path = pathname + (query ? `?${query}` : '') + (fragment ? `#${fragment}` : ''); - this._parser = await this._adapter.resolveRouterParser(); + this._parser = await getAdapter().resolveRouterParser(); this._state = await this._parser.parsePath(path); this._history = createMemoryHistory({ initialEntries: [path] }); @@ -62,8 +59,8 @@ export class Router extends EventEmitter { } // get the routerState for current url - public getState(): RouterState & { scheme: string } { - return { ...this._state!, scheme: this._adapter!.scheme }; + public getState(): RouterState { + return { ...this._state! }; } public getHistory() { @@ -118,9 +115,7 @@ export class Router extends EventEmitter { mergedState.path = `/${state.path?.split('/').filter(Boolean).join('/') || ''}`; } - return base - ? base.with(mergedState) - : vscode.Uri.from({ scheme: adapterManager.getCurrentScheme(), path: '/', ...mergedState }); + return base ? base.with(mergedState) : vscode.Uri.from({ scheme: getAdapter().scheme, path: '/', ...mergedState }); } } diff --git a/extensions/github1s/src/statusbar/sponsors.ts b/extensions/github1s/src/statusbar/sponsors.ts index 18cd0480d..60e78e32b 100644 --- a/extensions/github1s/src/statusbar/sponsors.ts +++ b/extensions/github1s/src/statusbar/sponsors.ts @@ -5,12 +5,12 @@ import * as vscode from 'vscode'; import router from '@/router'; -import { adapterManager } from '@/adapters'; +import { getAdapter } from '@/adapters'; import { PlatformName } from '@/adapters/types'; const resolveSourcegraphLink = async () => { const { repo, ref } = router.getState(); - switch (adapterManager.getCurrentAdapter().platformName) { + switch (getAdapter().platformName) { case PlatformName.GitHub: return `https://sourcegraph.com/github.com/${repo}@${ref}`; case PlatformName.GitLab: diff --git a/extensions/github1s/src/views/code-review-list.ts b/extensions/github1s/src/views/code-review-list.ts index 61c9f7957..9412be960 100644 --- a/extensions/github1s/src/views/code-review-list.ts +++ b/extensions/github1s/src/views/code-review-list.ts @@ -5,11 +5,9 @@ import * as vscode from 'vscode'; import * as queryString from 'query-string'; -import router from '@/router'; -import { Barrier } from '@/helpers/async'; import { Repository } from '@/repository'; +import { Barrier } from '@/helpers/async'; import { relativeTimeTo, toISOString } from '@/helpers/date'; -import adapterManager from '@/adapters/manager'; import * as adapterTypes from '@/adapters/types'; import { getChangedFileDiffCommand, getCodeReviewChangedFiles } from '@/changes/files'; import { GitHub1sSourceControlDecorationProvider } from '@/providers/decorations/source-control'; @@ -115,9 +113,8 @@ export class CodeReviewTreeDataProvider implements vscode.TreeDataProvider { + const repository = Repository.getCurrentInstance(); this._loadingBarrier && (await this._loadingBarrier.wait()); - const currentScheme = adapterManager.getCurrentScheme(); - const { repo } = router.getState(); - const repository = Repository.getInstance(currentScheme, repo); const codeReviews = await repository.getCodeReviewList(this._forceUpdate); const codeReviewTreeItems = codeReviews.map((codeReview) => { const label = getCodeReviewTreeItemLabel(codeReview); @@ -166,8 +160,8 @@ export class CodeReviewTreeDataProvider implements vscode.TreeDataProvider { - this._loadingBarrier && (await this._loadingBarrier.wait()); const repository = Repository.getCurrentInstance(); + this._loadingBarrier && (await this._loadingBarrier.wait()); const _codeReview = await repository.getCodeReviewItem(codeReview.id); const changedFiles = _codeReview ? await getCodeReviewChangedFiles(_codeReview) : []; const changedFileItems = changedFiles.map((changedFile) => { diff --git a/extensions/github1s/src/views/commit-list.ts b/extensions/github1s/src/views/commit-list.ts index e41a138d6..42392f8b8 100644 --- a/extensions/github1s/src/views/commit-list.ts +++ b/extensions/github1s/src/views/commit-list.ts @@ -5,11 +5,11 @@ import * as vscode from 'vscode'; import router from '@/router'; -import { Barrier } from '@/helpers/async'; +import { getAdapter } from '@/adapters'; import { Repository } from '@/repository'; +import { Barrier } from '@/helpers/async'; import * as queryString from 'query-string'; import { relativeTimeTo, toISOString } from '@/helpers/date'; -import adapterManager from '@/adapters/manager'; import * as adapterTypes from '@/adapters/types'; import { getChangedFileDiffCommand, getCommitChangedFiles } from '@/changes/files'; import { GitHub1sSourceControlDecorationProvider } from '@/providers/decorations/source-control'; @@ -74,9 +74,8 @@ export class CommitTreeDataProvider implements vscode.TreeDataProvider { this._loadingBarrier && (await this._loadingBarrier.wait()); const filePath = await this.resolveFilePath(); - const currentAdapter = adapterManager.getCurrentAdapter(); - const { repo, ref } = router.getState(); - const repository = Repository.getInstance(currentAdapter.scheme, repo); + const { ref } = router.getState(); + const repository = Repository.getCurrentInstance(); const repositoryCommits = await repository.getCommitList(ref, filePath, this._forceUpdate); const commitTreeItems = repositoryCommits.map((commit) => { const label = commit.message.split(/[\r\n]/)[0]; @@ -127,6 +124,7 @@ export class CommitTreeDataProvider implements vscode.TreeDataProvider { + const repository = Repository.getCurrentInstance(); const changedFiles = await getCommitChangedFiles(commit); const changedFileItems = changedFiles.map((changedFile) => { const filePath = changedFile.headFileUri.path; @@ -143,9 +141,6 @@ export class CommitTreeDataProvider implements vscode.TreeDataProvider { treeDataProvider: codeReviewRequestTreeDataProvider, }); // set code view list view title according code review type - const codeReviewType = adapterManager.getCurrentAdapter().codeReviewType || CodeReviewType.CodeReview; + const codeReviewType = getAdapter().codeReviewType || CodeReviewType.CodeReview; codeReviewTreeView.title = codeReviewViewTitle[codeReviewType]; context.subscriptions.push( From bd05cf149e803508405e26a3b335ab2595fea86d Mon Sep 17 00:00:00 2001 From: netcon Date: Wed, 12 Aug 2026 02:37:40 +0800 Subject: [PATCH 8/9] feat: fit resourceLabelFormatters (#716) --- .gitignore | 1 + extensions/github1s/package.json | 12 +++++----- .../github1s/src/providers/file-search.ts | 4 ++-- .../src/providers/file-system/index.ts | 6 ++--- extensions/github1s/src/router/authority.ts | 24 +++++++++++++++++++ extensions/github1s/src/router/index.ts | 10 ++++---- 6 files changed, 41 insertions(+), 16 deletions(-) create mode 100644 extensions/github1s/src/router/authority.ts diff --git a/.gitignore b/.gitignore index 9a28bffee..7f61e2ee3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ lib dist out node_modules +.worktrees/ diff --git a/extensions/github1s/package.json b/extensions/github1s/package.json index 927bdf563..6b1b26fcc 100644 --- a/extensions/github1s/package.json +++ b/extensions/github1s/package.json @@ -29,25 +29,25 @@ "resourceLabelFormatters": [ { "scheme": "github1s", - "authority": "**/*+?*", + "authority": "?*/**", "formatting": { - "label": "${path} (${authoritySuffix:7})", + "label": "${path} (${authoritySuffix})", "separator": "/" } }, { "scheme": "gitlab1s", - "authority": "**/*+?*", + "authority": "?*/**", "formatting": { - "label": "${path} (${authoritySuffix:7})", + "label": "${path} (${authoritySuffix})", "separator": "/" } }, { "scheme": "bitbucket1s", - "authority": "**/*+?*", + "authority": "?*/**", "formatting": { - "label": "${path} (${authoritySuffix:7})", + "label": "${path} (${authoritySuffix})", "separator": "/" } } diff --git a/extensions/github1s/src/providers/file-search.ts b/extensions/github1s/src/providers/file-search.ts index fd3ae75fa..2e651f109 100644 --- a/extensions/github1s/src/providers/file-search.ts +++ b/extensions/github1s/src/providers/file-search.ts @@ -57,7 +57,7 @@ export class GitHub1sFileSearchProvider implements FileSearchProvider, Disposabl */ getFileUris = reuseable(async (): Promise => { const { repo, ref } = router.getState(); - const cacheKey = `${repo}+${ref}`; + const cacheKey = `${repo}@${ref}`; if (this.fileUrisMap.has(cacheKey)) { return this.fileUrisMap.get(cacheKey)!; @@ -65,7 +65,7 @@ export class GitHub1sFileSearchProvider implements FileSearchProvider, Disposabl const dataSource = await getAdapter().resolveDataSource(); const rootDirectoryData = await dataSource.provideDirectory(repo, ref, '/', true); - const rootDirectoryUri = router.buildUri({ repo, ref, path: '/' }); + const rootDirectoryUri = router.buildUri({ path: '/' }); // the number of items in the tree array maybe exceeded maximum limit, only // insert the data to fileSystemProvider's cache if `treeData.truncated` is false diff --git a/extensions/github1s/src/providers/file-system/index.ts b/extensions/github1s/src/providers/file-system/index.ts index 508dc2f24..eb3f29f08 100644 --- a/extensions/github1s/src/providers/file-system/index.ts +++ b/extensions/github1s/src/providers/file-system/index.ts @@ -86,7 +86,7 @@ export class GitHub1sFileSystemProvider implements FileSystemProvider, Disposabl public async lookup(uri: Uri, silent: boolean): Promise { const parts = uri.path.split('/').filter(Boolean); const { scheme, repo, ref } = router.parseUri(uri); - const lookupKey = `${scheme}:${repo}+${ref}`; + const lookupKey = `${scheme}:${repo}@${ref}`; if (!this.root.has(lookupKey)) { this.root.set(lookupKey, createEntry(adapterTypes.FileType.Directory, uri.with({ path: '/' }), '')); } @@ -172,7 +172,7 @@ export class GitHub1sFileSystemProvider implements FileSystemProvider, Disposabl } const subRef = directory.sha || 'HEAD'; const [subScheme, subRepo] = await parseSubmoduleUrl(gitmoduleData.url); - const lookupKey = `${subScheme}:${subRepo}+${subRef}`; + const lookupKey = `${subScheme}:${subRepo}@${subRef}`; directory.name = ''; // update the name field to '' to indicated it is an root directory // update the uri field to indicated it is belong the `submodule repository` directory.uri = router.buildUri({ scheme: subScheme, repo: subRepo, ref: subRef, path: '/' }); @@ -215,7 +215,7 @@ export class GitHub1sFileSystemProvider implements FileSystemProvider, Disposabl uri = Uri.joinPath(file.uri, file.name); } const { scheme, repo, ref, path } = router.parseUri(uri); - const cacheKey = `${scheme}:${repo}+${ref}${path}`; + const cacheKey = `${scheme}:${repo}@${ref}${path}`; if (!this.contentCache.has(cacheKey)) { const dataSource = await getAdapter(scheme).resolveDataSource(); const data = await dataSource.provideFile(repo, ref, path); diff --git a/extensions/github1s/src/router/authority.ts b/extensions/github1s/src/router/authority.ts new file mode 100644 index 000000000..9472c0468 --- /dev/null +++ b/extensions/github1s/src/router/authority.ts @@ -0,0 +1,24 @@ +/** + * @file URI authority helpers + * @author netcon + */ + +export const buildAuthority = (repo: string, ref: string): string => { + // label is using for display in resourceLabelFormatters + const label = ref.length >= 32 ? ref.slice(0, 7) : ref; + return repo && ref ? `${repo}@${ref}+${label}` : ''; +}; + +export const parseAuthority = (authority: string): { repo: string; ref: string } | undefined => { + // repo name may starts with @, so we skip the first character + const atIndex = authority.slice(1).indexOf('@') + 1; + if (atIndex <= 0) { + // compatible with old format, remove in the future + const [repo, ref] = authority.split('+'); + return repo && ref ? { repo, ref } : undefined; + } + const repo = authority.slice(0, atIndex); + const plusIndex = authority.lastIndexOf('+'); + const ref = plusIndex > 0 ? authority.slice(atIndex + 1, plusIndex) : ''; + return repo && ref ? { repo, ref } : undefined; +}; diff --git a/extensions/github1s/src/router/index.ts b/extensions/github1s/src/router/index.ts index 9062c360f..432e9b342 100644 --- a/extensions/github1s/src/router/index.ts +++ b/extensions/github1s/src/router/index.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import { getAdapter } from '@/adapters'; import { History, createMemoryHistory, parsePath, Action } from 'history'; import { RouterParser, RouterState } from '@/adapters/types'; +import { buildAuthority, parseAuthority } from './authority'; import { EventEmitter } from './events'; export interface UrlManager { @@ -93,9 +94,8 @@ export class Router extends EventEmitter { } public parseUri(uri: vscode.Uri): UriState { - const scheme = uri.scheme; - const [repo, ref] = uri.authority ? uri.authority.split('+') : [this._state!.repo, this._state!.ref]; - return { scheme, repo, ref, path: uri.path || '/' }; + const { repo, ref } = parseAuthority(uri.authority) || this._state!; + return { scheme: uri.scheme, repo, ref, path: uri.path || '/' }; } public buildUri(state?: Partial, base?: vscode.Uri): vscode.Uri { @@ -108,8 +108,8 @@ export class Router extends EventEmitter { throw new Error('ref is required when repo is provided'); } if (state?.hasOwnProperty('ref')) { - const repo = state.repo || base?.authority.split('+')[0] || this._state!.repo; - mergedState.authority = repo && state.ref ? `${repo}+${state.ref}` : ''; + const repo = state.repo || parseAuthority(base?.authority || '')?.repo || this._state!.repo; + mergedState.authority = buildAuthority(repo, state.ref || ''); } if (state?.hasOwnProperty('path')) { mergedState.path = `/${state.path?.split('/').filter(Boolean).join('/') || ''}`; From c9db9a7cbcd3da65ce4f4bc54459b4969c51bd69 Mon Sep 17 00:00:00 2001 From: netcon Date: Wed, 12 Aug 2026 14:11:40 +0800 Subject: [PATCH 9/9] chore: bump vscode to 1.132.1 (#717) --- .husky/pre-commit | 3 --- package-lock.json | 8 ++++---- package.json | 2 +- vscode-web/.VERSION | 2 +- vscode-web/package-lock.json | 4 ++-- vscode-web/package.json | 2 +- .../services/label/common/labelService.ts | 14 +++----------- 7 files changed, 12 insertions(+), 23 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af21989..2312dc587 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npx lint-staged diff --git a/package-lock.json b/package-lock.json index e08421bc0..afa203c83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "ISC", "devDependencies": { "@cloudflare/workers-types": "^4.20250109.0", - "@github1s/vscode-web": "^0.29.0", + "@github1s/vscode-web": "^0.30.1", "chokidar": "^4.0.3", "clean-css": "^5.3.3", "copy-webpack-plugin": "^14.0.0", @@ -251,9 +251,9 @@ } }, "node_modules/@github1s/vscode-web": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/@github1s/vscode-web/-/vscode-web-0.29.0.tgz", - "integrity": "sha512-e0d+8B8F1rdDjoCSCw8F08R22MEXpmnQ5iN8H/d/WzUDDf6igiMWOnVhKOlF3IO0rbMHs5cZEsSCbkRv08v2OA==", + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/@github1s/vscode-web/-/vscode-web-0.30.1.tgz", + "integrity": "sha512-W4vzRSdqc5UcYbAA9fgfTd2KJerNthoY6Z/J34x7Qla9TGbLCJmiheCbzKG2KG3ku1N40XNmK7H+POL6waL67A==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index bf9256fc6..81c533a46 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "license": "ISC", "devDependencies": { "@cloudflare/workers-types": "^4.20250109.0", - "@github1s/vscode-web": "^0.29.0", + "@github1s/vscode-web": "^0.30.1", "chokidar": "^4.0.3", "clean-css": "^5.3.3", "copy-webpack-plugin": "^14.0.0", diff --git a/vscode-web/.VERSION b/vscode-web/.VERSION index 12f5b64c8..e351d98a8 100644 --- a/vscode-web/.VERSION +++ b/vscode-web/.VERSION @@ -1 +1 @@ -1.132.0 \ No newline at end of file +1.132.1 \ No newline at end of file diff --git a/vscode-web/package-lock.json b/vscode-web/package-lock.json index da933de2c..950a9662b 100644 --- a/vscode-web/package-lock.json +++ b/vscode-web/package-lock.json @@ -1,12 +1,12 @@ { "name": "@github1s/vscode-web", - "version": "0.29.0", + "version": "0.30.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@github1s/vscode-web", - "version": "0.29.0", + "version": "0.30.1", "license": "MIT", "devDependencies": { "chokidar": "^4.0.3", diff --git a/vscode-web/package.json b/vscode-web/package.json index 970031796..db106c6e5 100644 --- a/vscode-web/package.json +++ b/vscode-web/package.json @@ -1,6 +1,6 @@ { "name": "@github1s/vscode-web", - "version": "0.29.0", + "version": "0.30.1", "description": "VS Code web for GitHub1s", "author": "github1s", "license": "MIT", diff --git a/vscode-web/src/vs/workbench/services/label/common/labelService.ts b/vscode-web/src/vs/workbench/services/label/common/labelService.ts index 7832cb933..2557d4b4f 100644 --- a/vscode-web/src/vs/workbench/services/label/common/labelService.ts +++ b/vscode-web/src/vs/workbench/services/label/common/labelService.ts @@ -77,9 +77,7 @@ const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoin const posixPathSeparatorRegexp = /\//g; // on Unix, backslash is a valid filename character const winPathSeparatorRegexp = /[\\\/]/g; // on Windows, neither slash nor backslash are valid filename characters -// below codes are changed by github1s -const labelMatchingRegexp = /\$\{(scheme|authoritySuffix(?::\d+)?|authority|path|(query)\.(.+?))\}/g; -// above codes are changed by github1s +const labelMatchingRegexp = /\$\{(scheme|authoritySuffix|authority|path|(query)\.(.+?))\}/g; function hasDriveLetterIgnorePlatform(path: string): boolean { return !!(path && path[2] === ':'); @@ -454,19 +452,13 @@ export class LabelService extends Disposable implements ILabelService { } private formatUri(resource: URI, formatting: ResourceLabelFormatting, forceNoTildify?: boolean): string { - // below codes are changed by github1s - let label = formatting.label.replace(labelMatchingRegexp, (match, tokenWithArgument, qsToken, qsValue) => { - const [token, argument] = tokenWithArgument.split(':'); - // above codes are changed by github1s + let label = formatting.label.replace(labelMatchingRegexp, (match, token, qsToken, qsValue) => { switch (token) { case 'scheme': return resource.scheme; case 'authority': return resource.authority; case 'authoritySuffix': { const i = resource.authority.indexOf('+'); - // below codes are changed by github1s - const authoritySuffix = i === -1 ? resource.authority : resource.authority.slice(i + 1); - return argument === undefined ? authoritySuffix : authoritySuffix.slice(0, Number(argument)); - // above codes are changed by github1s + return i === -1 ? resource.authority : resource.authority.slice(i + 1); } case 'path': { let pathValue = resource.path;