Skip to content

Commit

Permalink
implement basic code highlighting with pygments
Browse files Browse the repository at this point in the history
- use interface that permits multiple highlighters in the future
  • Loading branch information
Totto16 committed Apr 21, 2024
1 parent 141b7d7 commit 3001330
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 24 deletions.
8 changes: 7 additions & 1 deletion src/components/codePanoItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ export class CodePanoItem extends PanoItem {
this.body.set_style(`background-color: ${bodyBgColor}`);
this.label.set_style(`font-size: ${bodyFontSize}px; font-family: ${bodyFontFamily};`);

this.label.clutterText.set_markup(markupCode(this.language, this.dbItem.content.trim(), characterLength));
const markup = markupCode(this.language, this.dbItem.content.trim(), characterLength);

if (!markup) {
throw new Error("Couldn't generate markup");
}

this.label.clutterText.set_markup(markup);
}

private setClipboardContent(): void {
Expand Down
22 changes: 22 additions & 0 deletions src/utils/code/highlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type Language = {
relevance: number;
language: string;
};

export type CodeHighlighterType = 'CommandLine' | 'Integrated' | 'JSModule';

export abstract class CodeHighlighter {
readonly name: string;
readonly type: CodeHighlighterType;

constructor(name: string, type: CodeHighlighterType) {
this.name = name;
this.type = type;
}

abstract isInstalled(): boolean;

abstract detectLanguage(text: string): Language | undefined;

abstract markupCode(language: string, text: string, characterLength: number): string | undefined;
}
88 changes: 88 additions & 0 deletions src/utils/code/pygments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { CodeHighlighter, type Language } from '@pano/utils/code/highlight';
import { logger } from '@pano/utils/shell';
import Gio from 'gi://Gio?version=2.0';

const debug = logger('code-highlighter:pygments');

export class PygmentsCodeHighlighter extends CodeHighlighter {
private cliName = 'pygmentize';

constructor() {
super('pygments', 'CommandLine');
}

override isInstalled(): boolean {
try {
const proc = Gio.Subprocess.new(
['which', this.cliName],
Gio.SubprocessFlags.STDOUT_SILENCE | Gio.SubprocessFlags.STDERR_SILENCE,
);

const success = proc.wait(null);
if (!success) {
throw new Error('Process was cancelled');
}

return proc.get_successful();
} catch (err) {
debug(`An error occurred while testing for the executable: ${err}`);
return false;
}
}

override detectLanguage(text: string): Language | undefined {
const noLanguageDetected = 'text';

try {
const proc = Gio.Subprocess.new(
[this.cliName, '-C'],
Gio.SubprocessFlags.STDERR_PIPE | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDIN_PIPE,
);
const [success, stdout, stderr] = proc.communicate_utf8(text, null);

if (!success) {
throw new Error('Process was cancelled');
}

if (proc.get_successful()) {
const content = stdout.trim();

if (content === noLanguageDetected) {
return undefined;
}

return { language: content, relevance: 1.0 };
} else {
throw new Error(`Process exited with exit code: ${proc.get_exit_status()} and output: ${stderr}`);
}
} catch (err) {
debug(`An error occurred while detecting the language: ${err}`);
return undefined;
}
}

override markupCode(language: string, text: string, characterLength: number): string | undefined {
const finalText = text.substring(0, characterLength);

try {
const proc = Gio.Subprocess.new(
[this.cliName, '-l', language, '-f', 'pango'],
Gio.SubprocessFlags.STDERR_PIPE | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDIN_PIPE,
);
const [success, stdout, stderr] = proc.communicate_utf8(finalText, null);

if (!success) {
throw new Error('Process was cancelled');
}

if (proc.get_successful()) {
return stdout;
} else {
throw new Error(`Process exited with exit code: ${proc.get_exit_status()} and output: ${stderr}`);
}
} catch (err) {
debug(`An error occurred while formatting the language: ${err}`);
return undefined;
}
}
}
71 changes: 58 additions & 13 deletions src/utils/pango.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,64 @@
import { logger } from '@pano/utils/shell';
import type { CodeHighlighter, Language } from '@pano/utils/code/highlight';
import { PygmentsCodeHighlighter } from '@pano/utils/code/pygments';

const debug = logger('pango');
//TODO:
// add highlight.js back, if it is installed and can be found via require()
// add settings, that might change theses things:
// which doe formatter to use, which style to use
// enable, disable formatting, change the threshold (event if its always 1.0 in e.g pygmentize)
// only make these count, if enabled is set, so if at least one formatter is found
// button to recheck tools
// customs settings per highlighter

export type Language = {
relevance: number;
language: string;
};
let detectedHighlighter: CodeHighlighter[] | null = null;

export function detectLanguage(_text: string): Language | undefined {
//TODO: implement language detection
return undefined;
let currentHighlighter: CodeHighlighter | null = null;

const availableCodeHighlighter: CodeHighlighter[] = [new PygmentsCodeHighlighter()];

// this is only implicitly called once, even if nothing is found, it isn't called again later, it has to be initiated by the user later, to scan again
export function detectHighlighter(force = false, preferredHighlighter: string | null = null) {
if (detectedHighlighter !== null && !force) {
return;
}

detectedHighlighter = [];

for (const codeHighlighter of availableCodeHighlighter) {
if (codeHighlighter.isInstalled()) {
detectedHighlighter.push(codeHighlighter);

if (preferredHighlighter === null) {
if (currentHighlighter === null) {
currentHighlighter = codeHighlighter;
}
} else if (codeHighlighter.name == preferredHighlighter) {
currentHighlighter = codeHighlighter;
}
}
}
}

export function markupCode(_language: string, _text: string, _characterLength: number): string {
//TODO implement code highlighting
debug('TODO');
return '';
export function detectLanguage(text: string): Language | undefined {
if (detectedHighlighter === null) {
detectHighlighter();
}

if (currentHighlighter === null) {
return undefined;
}

return currentHighlighter.detectLanguage(text);
}

export function markupCode(language: string, text: string, characterLength: number): string | undefined {
if (detectedHighlighter === null) {
detectHighlighter();
}

if (currentHighlighter === null) {
return undefined;
}

return currentHighlighter.markupCode(language, text, characterLength);
}
18 changes: 10 additions & 8 deletions src/utils/panoItemFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { TextPanoItem } from '@pano/components/textPanoItem';
import { ClipboardContent, ClipboardManager, ContentType, FileOperation } from '@pano/utils/clipboardManager';
import { ClipboardQueryBuilder, db, DBItem } from '@pano/utils/db';
import { getDocument, getImage } from '@pano/utils/linkParser';
import { detectLanguage } from '@pano/utils/pango';
import {
getCachePath,
getCurrentExtensionSettings,
Expand All @@ -30,10 +31,9 @@ import isUrl from 'is-url';
import prettyBytes from 'pretty-bytes';
import { validateHTMLColorHex, validateHTMLColorName, validateHTMLColorRgb } from 'validate-color';

import { detectLanguage } from './pango';

const debug = logger('pano-item-factory');

//TODO: make configurable
const MINIMUM_LANGUAGE_RELEVANCE = 0.1;

const isValidUrl = (text: string) => {
Expand Down Expand Up @@ -235,11 +235,7 @@ export const createPanoItem = async (

if (dbItem) {
if (getCurrentExtensionSettings(ext).get_boolean('send-notification-on-copy')) {
try {
sendNotification(ext, dbItem);
} catch (err) {
console.error('PANO: ' + (err as Error).toString());
}
sendNotification(ext, dbItem);
}

return createPanoItemFromDb(ext, clipboardManager, dbItem);
Expand Down Expand Up @@ -279,7 +275,13 @@ export const createPanoItemFromDb = (
}

if (language) {
panoItem = new CodePanoItem(ext, clipboardManager, dbItem, language);
try {
panoItem = new CodePanoItem(ext, clipboardManager, dbItem, language);
// this might fail in some really rare cases
} catch (err) {
debug(`Couldn't create a code item: ${err}`);
panoItem = new TextPanoItem(ext, clipboardManager, dbItem);
}
} else {
panoItem = new TextPanoItem(ext, clipboardManager, dbItem);
}
Expand Down
3 changes: 1 addition & 2 deletions src/utils/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import * as main from '@girs/gnome-shell/dist/ui/main';
import type { Notification, Source as MessageTraySource } from '@girs/gnome-shell/dist/ui/messageTray';
import Shell from '@girs/shell-14';
import St from '@girs/st-14';
import { addNotification, newMessageTraySource, newNotification } from '@pano/utils/compatibility';
import { gettext } from '@pano/utils/shell';

import { addNotification, newMessageTraySource, newNotification } from './compatibility';

const global = Shell.Global.get();

export const notify = (
Expand Down

0 comments on commit 3001330

Please sign in to comment.