Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into feat/ai-chat-cont…
Browse files Browse the repository at this point in the history
…ribution
  • Loading branch information
sdirix committed Aug 13, 2024
2 parents 4123fc7 + a9345da commit 64c38f5
Show file tree
Hide file tree
Showing 62 changed files with 1,871 additions and 487 deletions.
2 changes: 1 addition & 1 deletion dependency-check-baseline.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"npm/npmjs/@types/qs/6.9.11": "Pending https://gitlab.eclipse.org/eclipsefdn/emo-team/iplab/-/issues/13990"
"npm/npmjs/-/advanced-mark.js/2.6.0": "Manually approved"
}
11 changes: 6 additions & 5 deletions dev-packages/cli/src/download-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ export interface DownloadPluginsOptions {
* Fetch plugins in parallel
*/
parallel?: boolean;

rateLimit?: number;
}

interface PluginDownload {
Expand All @@ -65,16 +63,19 @@ interface PluginDownload {
version?: string | undefined
}

export default async function downloadPlugins(ovsxClient: OVSXClient, requestService: RequestService, options: DownloadPluginsOptions = {}): Promise<void> {
export default async function downloadPlugins(
ovsxClient: OVSXClient,
rateLimiter: RateLimiter,
requestService: RequestService,
options: DownloadPluginsOptions = {}
): Promise<void> {
const {
packed = false,
ignoreErrors = false,
apiVersion = DEFAULT_SUPPORTED_API_VERSION,
rateLimit = 15,
parallel = true
} = options;

const rateLimiter = new RateLimiter({ tokensPerInterval: rateLimit, interval: 'second' });
const apiFilter = new OVSXApiFilterImpl(ovsxClient, apiVersion);

// Collect the list of failures to be appended at the end of the script.
Expand Down
12 changes: 7 additions & 5 deletions dev-packages/cli/src/theia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ import { ApplicationProps, DEFAULT_SUPPORTED_API_VERSION } from '@theia/applicat
import checkDependencies from './check-dependencies';
import downloadPlugins from './download-plugins';
import runTest from './run-test';
import { RateLimiter } from 'limiter';
import { LocalizationManager, extract } from '@theia/localization-manager';
import { NodeRequestService } from '@theia/request/lib/node-request-service';
import { ExtensionIdMatchesFilterFactory, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory } from '@theia/ovsx-client';
import { ExtensionIdMatchesFilterFactory, OVSX_RATE_LIMIT, OVSXClient, OVSXHttpClient, OVSXRouterClient, RequestContainsFilterFactory } from '@theia/ovsx-client';

const { executablePath } = require('puppeteer');

Expand Down Expand Up @@ -389,7 +390,7 @@ async function theiaCli(): Promise<void> {
'rate-limit': {
describe: 'Amount of maximum open-vsx requests per second',
number: true,
default: 15
default: OVSX_RATE_LIMIT
},
'proxy-url': {
describe: 'Proxy URL'
Expand All @@ -415,22 +416,23 @@ async function theiaCli(): Promise<void> {
strictSSL: strictSsl
});
let client: OVSXClient | undefined;
const rateLimiter = new RateLimiter({ tokensPerInterval: options.rateLimit, interval: 'second' });
if (ovsxRouterConfig) {
const routerConfig = await fs.promises.readFile(ovsxRouterConfig, 'utf8').then(JSON.parse, error => {
console.error(error);
});
if (routerConfig) {
client = await OVSXRouterClient.FromConfig(
routerConfig,
OVSXHttpClient.createClientFactory(requestService),
OVSXHttpClient.createClientFactory(requestService, rateLimiter),
[RequestContainsFilterFactory, ExtensionIdMatchesFilterFactory]
);
}
}
if (!client) {
client = new OVSXHttpClient(apiUrl, requestService);
client = new OVSXHttpClient(apiUrl, requestService, rateLimiter);
}
await downloadPlugins(client, requestService, options);
await downloadPlugins(client, rateLimiter, requestService, options);
},
})
.command<{
Expand Down
1 change: 1 addition & 0 deletions dev-packages/ovsx-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
},
"dependencies": {
"@theia/request": "1.52.0",
"limiter": "^2.1.0",
"semver": "^7.5.4",
"tslib": "^2.6.2"
}
Expand Down
2 changes: 1 addition & 1 deletion dev-packages/ovsx-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// *****************************************************************************

export { OVSXApiFilter, OVSXApiFilterImpl, OVSXApiFilterProvider } from './ovsx-api-filter';
export { OVSXHttpClient } from './ovsx-http-client';
export { OVSXHttpClient, OVSX_RATE_LIMIT } from './ovsx-http-client';
export { OVSXMockClient } from './ovsx-mock-client';
export { OVSXRouterClient, OVSXRouterConfig, OVSXRouterFilterFactory as FilterFactory } from './ovsx-router-client';
export * from './ovsx-router-filters';
Expand Down
5 changes: 4 additions & 1 deletion dev-packages/ovsx-client/src/ovsx-api-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,13 @@ export class OVSXApiFilterImpl implements OVSXApiFilter {

protected async queryLatestCompatibleExtension(query: VSXQueryOptions): Promise<VSXExtensionRaw | undefined> {
let offset = 0;
let size = 5;
let loop = true;
while (loop) {
const queryOptions: VSXQueryOptions = {
...query,
offset,
size: 5 // there is a great chance that the newest version will work
size // there is a great chance that the newest version will work
};
const results = await this.client.query(queryOptions);
const compatibleExtension = this.getLatestCompatibleExtension(results.extensions);
Expand All @@ -83,6 +84,8 @@ export class OVSXApiFilterImpl implements OVSXApiFilter {
offset += results.extensions.length;
// Continue querying if there are more extensions available
loop = results.totalSize > offset;
// Adjust the size to fetch more extensions next time
size = Math.min(size * 2, 100);
}
return undefined;
}
Expand Down
33 changes: 26 additions & 7 deletions dev-packages/ovsx-client/src/ovsx-http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,26 @@

import { OVSXClient, VSXQueryOptions, VSXQueryResult, VSXSearchOptions, VSXSearchResult } from './ovsx-types';
import { RequestContext, RequestService } from '@theia/request';
import { RateLimiter } from 'limiter';

export const OVSX_RATE_LIMIT = 15;

export class OVSXHttpClient implements OVSXClient {

/**
* @param requestService
* @returns factory that will cache clients based on the requested input URL.
*/
static createClientFactory(requestService: RequestService): (url: string) => OVSXClient {
static createClientFactory(requestService: RequestService, rateLimiter?: RateLimiter): (url: string) => OVSXClient {
// eslint-disable-next-line no-null/no-null
const cachedClients: Record<string, OVSXClient> = Object.create(null);
return url => cachedClients[url] ??= new this(url, requestService);
return url => cachedClients[url] ??= new this(url, requestService, rateLimiter);
}

constructor(
protected vsxRegistryUrl: string,
protected requestService: RequestService
protected requestService: RequestService,
protected rateLimiter = new RateLimiter({ tokensPerInterval: OVSX_RATE_LIMIT, interval: 'second' })
) { }

search(searchOptions?: VSXSearchOptions): Promise<VSXSearchResult> {
Expand All @@ -43,10 +47,25 @@ export class OVSXHttpClient implements OVSXClient {
}

protected async requestJson<R>(url: string): Promise<R> {
return RequestContext.asJson<R>(await this.requestService.request({
url,
headers: { 'Accept': 'application/json' }
}));
const attempts = 5;
for (let i = 0; i < attempts; i++) {
// Use 1, 2, 4, 8, 16 tokens for each attempt
const tokenCount = Math.pow(2, i);
await this.rateLimiter.removeTokens(tokenCount);
const context = await this.requestService.request({
url,
headers: { 'Accept': 'application/json' }
});
if (context.res.statusCode === 429) {
console.warn('OVSX rate limit exceeded. Consider reducing the rate limit.');
// If there are still more attempts left, retry the request with a higher token count
if (i < attempts - 1) {
continue;
}
}
return RequestContext.asJson<R>(context);
}
throw new Error('Failed to fetch data from OVSX.');
}

protected buildUrl(url: string, query?: object): string {
Expand Down
2 changes: 1 addition & 1 deletion examples/playwright/src/tests/theia-main-menu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ test.describe('Theia Main Menu', () => {
await (await menuBar.openMenu('Help')).clickMenuItem('About');
const aboutDialog = new TheiaAboutDialog(app);
expect(await aboutDialog.isVisible()).toBe(true);
await aboutDialog.page.getByRole('button', { name: 'OK' }).click();
await aboutDialog.page.locator('#theia-dialog-shell').getByRole('button', { name: 'OK' }).click();
expect(await aboutDialog.isVisible()).toBe(false);
});

Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { environment } from '../common';
import { Disposable, environment } from '../common';

const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';

Expand Down Expand Up @@ -228,3 +228,12 @@ function getMeasurementElement(style?: PartialCSSStyle): HTMLElement {
}
return measureElement;
}

export function onDomEvent<K extends keyof HTMLElementEventMap>(
element: Node,
type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => unknown,
options?: boolean | AddEventListenerOptions): Disposable {
element.addEventListener(type, listener, options);
return { dispose: () => element.removeEventListener(type, listener, options) };
}
115 changes: 112 additions & 3 deletions packages/core/src/browser/common-frontend-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { UserWorkingDirectoryProvider } from './user-working-directory-provider'
import { UNTITLED_SCHEME, UntitledResourceResolver } from '../common';
import { LanguageQuickPickService } from './i18n/language-quick-pick-service';
import { SidebarMenu } from './shell/sidebar-menu-widget';
import { UndoRedoHandlerService } from './undo-redo-handler';

export namespace CommonMenus {

Expand Down Expand Up @@ -443,7 +444,11 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
@inject(UntitledResourceResolver)
protected readonly untitledResourceResolver: UntitledResourceResolver;

@inject(UndoRedoHandlerService)
protected readonly undoRedoHandlerService: UndoRedoHandlerService;

protected pinnedKey: ContextKey<boolean>;
protected inputFocus: ContextKey<boolean>;

async configure(app: FrontendApplication): Promise<void> {
// FIXME: This request blocks valuable startup time (~200ms).
Expand All @@ -458,6 +463,9 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
this.contextKeyService.createKey<boolean>('isMac', OS.type() === OS.Type.OSX);
this.contextKeyService.createKey<boolean>('isWindows', OS.type() === OS.Type.Windows);
this.contextKeyService.createKey<boolean>('isWeb', !this.isElectron());
this.inputFocus = this.contextKeyService.createKey<boolean>('inputFocus', false);
this.updateInputFocus();
browser.onDomEvent(document, 'focusin', () => this.updateInputFocus());

this.pinnedKey = this.contextKeyService.createKey<boolean>('activeEditorIsPinned', false);
this.updatePinnedKey();
Expand Down Expand Up @@ -513,6 +521,15 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
}
}

protected updateInputFocus(): void {
const activeElement = document.activeElement;
if (activeElement) {
const isInput = activeElement.tagName?.toLowerCase() === 'input'
|| activeElement.tagName?.toLowerCase() === 'textarea';
this.inputFocus.set(isInput);
}
}

protected updatePinnedKey(): void {
const activeTab = this.shell.findTabBar();
const pinningTarget = activeTab && this.shell.findTitle(activeTab);
Expand Down Expand Up @@ -801,10 +818,14 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
}));

commandRegistry.registerCommand(CommonCommands.UNDO, {
execute: () => document.execCommand('undo')
execute: () => {
this.undoRedoHandlerService.undo();
}
});
commandRegistry.registerCommand(CommonCommands.REDO, {
execute: () => document.execCommand('redo')
execute: () => {
this.undoRedoHandlerService.redo();
}
});
commandRegistry.registerCommand(CommonCommands.SELECT_ALL, {
execute: () => document.execCommand('selectAll')
Expand Down Expand Up @@ -1067,7 +1088,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
},
{
command: CommonCommands.REDO.id,
keybinding: 'ctrlcmd+shift+z'
keybinding: isOSX ? 'ctrlcmd+shift+z' : 'ctrlcmd+y'
},
{
command: CommonCommands.SELECT_ALL.id,
Expand Down Expand Up @@ -1899,6 +1920,94 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
}, description: 'Status bar warning items foreground color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.'
},

// editor find

{
id: 'editor.findMatchBackground',
defaults: {
light: '#A8AC94',
dark: '#515C6A',
hcDark: undefined,
hcLight: undefined
},
description: 'Color of the current search match.'
},

{
id: 'editor.findMatchForeground',
defaults: {
light: undefined,
dark: undefined,
hcDark: undefined,
hcLight: undefined
},
description: 'Text color of the current search match.'
},
{
id: 'editor.findMatchHighlightBackground',
defaults: {
light: '#EA5C0055',
dark: '#EA5C0055',
hcDark: undefined,
hcLight: undefined
},
description: 'Color of the other search matches. The color must not be opaque so as not to hide underlying decorations.'
},

{
id: 'editor.findMatchHighlightForeground',
defaults: {
light: undefined,
dark: undefined,
hcDark: undefined,
hcLight: undefined
},
description: 'Foreground color of the other search matches.'
},

{
id: 'editor.findRangeHighlightBackground',
defaults: {
dark: '#3a3d4166',
light: '#b4b4b44d',
hcDark: undefined,
hcLight: undefined
},
description: 'Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.'
},

{
id: 'editor.findMatchBorder',
defaults: {
light: undefined,
dark: undefined,
hcDark: 'activeContrastBorder',
hcLight: 'activeContrastBorder'
},
description: 'Border color of the current search match.'
},
{
id: 'editor.findMatchHighlightBorder',
defaults: {
light: undefined,
dark: undefined,
hcDark: 'activeContrastBorder',
hcLight: 'activeContrastBorder'
},
description: 'Border color of the other search matches.'
},

{
id: 'editor.findRangeHighlightBorder',
defaults: {
dark: undefined,
light: undefined,
hcDark: Color.transparent('activeContrastBorder', 0.4),
hcLight: Color.transparent('activeContrastBorder', 0.4)
},
description: 'Border color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.'
},

// Quickinput colors should be aligned with https://code.visualstudio.com/api/references/theme-color#quick-picker
// if not yet contributed by Monaco, check runtime css variables to learn.
{
Expand Down
Loading

0 comments on commit 64c38f5

Please sign in to comment.