From 30013304af0224c9bcbf5f0011787b710792a27d Mon Sep 17 00:00:00 2001 From: Totto16 Date: Sun, 21 Apr 2024 03:29:38 +0200 Subject: [PATCH] implement basic code highlighting with pygments - use interface that permits multiple highlighters in the future --- src/components/codePanoItem.ts | 8 +++- src/utils/code/highlight.ts | 22 +++++++++ src/utils/code/pygments.ts | 88 ++++++++++++++++++++++++++++++++++ src/utils/pango.ts | 71 ++++++++++++++++++++++----- src/utils/panoItemFactory.ts | 18 +++---- src/utils/ui.ts | 3 +- 6 files changed, 186 insertions(+), 24 deletions(-) create mode 100644 src/utils/code/highlight.ts create mode 100644 src/utils/code/pygments.ts diff --git a/src/components/codePanoItem.ts b/src/components/codePanoItem.ts index 76693056..1a751893 100644 --- a/src/components/codePanoItem.ts +++ b/src/components/codePanoItem.ts @@ -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 { diff --git a/src/utils/code/highlight.ts b/src/utils/code/highlight.ts new file mode 100644 index 00000000..7b617e90 --- /dev/null +++ b/src/utils/code/highlight.ts @@ -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; +} diff --git a/src/utils/code/pygments.ts b/src/utils/code/pygments.ts new file mode 100644 index 00000000..b1737bcc --- /dev/null +++ b/src/utils/code/pygments.ts @@ -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; + } + } +} diff --git a/src/utils/pango.ts b/src/utils/pango.ts index 4d61c7a6..e1b6d60a 100644 --- a/src/utils/pango.ts +++ b/src/utils/pango.ts @@ -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); } diff --git a/src/utils/panoItemFactory.ts b/src/utils/panoItemFactory.ts index c16a8e27..e47077f2 100644 --- a/src/utils/panoItemFactory.ts +++ b/src/utils/panoItemFactory.ts @@ -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, @@ -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) => { @@ -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); @@ -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); } diff --git a/src/utils/ui.ts b/src/utils/ui.ts index 9506699a..89b45d70 100644 --- a/src/utils/ui.ts +++ b/src/utils/ui.ts @@ -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 = (