2 Commits

Author SHA1 Message Date
1381696f9b editor: File extension-based language detection.
All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m42s
2025-12-02 19:26:13 -05:00
19b346cc6d ssb: Move the search to an ever-present textbox on the menu bar. 2025-12-02 18:47:58 -05:00
8 changed files with 152 additions and 43 deletions

View File

@@ -1,5 +1,5 @@
{ {
"type": "tildefriends-app", "type": "tildefriends-app",
"emoji": "🦀", "emoji": "🦀",
"previous": "&W1tVCm6k+sTaIprqugaZlifh/FPQWCqGXXHn1hojCHI=.sha256" "previous": "&NdaJseW11fi9Cj4m+WITGxOF2JExm6GDhdCJKFV284Y=.sha256"
} }

View File

@@ -752,6 +752,16 @@ class TfElement extends LitElement {
input.click(); input.click();
} }
search() {
this.set_hash('#q=' + this.renderRoot.getElementById('search_text').value);
}
search_keydown(event) {
if (event.keyCode == 13) {
this.search();
}
}
render() { render() {
let self = this; let self = this;
@@ -765,7 +775,6 @@ class TfElement extends LitElement {
const k_tabs = { const k_tabs = {
'📰': 'news', '📰': 'news',
'📡': 'connections', '📡': 'connections',
'🔍': 'search',
}; };
let tabs = html` let tabs = html`
@@ -773,25 +782,27 @@ class TfElement extends LitElement {
class="w3-bar w3-theme-l1" class="w3-bar w3-theme-l1"
style="position: static; top: 0; z-index: 10" style="position: static; top: 0; z-index: 10"
> >
${this.is_administrator ${
? html` this.is_administrator
<button ? html`
class=${'w3-bar-item w3-button w3-circle w3-ripple' + <button
(this.connections?.some((x) => x.flags.one_shot) class=${'w3-bar-item w3-button w3-circle w3-ripple' +
? ' w3-spin' (this.connections?.some((x) => x.flags.one_shot)
: '')} ? ' w3-spin'
@click=${this.refresh} : '')}
> @click=${this.refresh}
>
</button>
<button </button>
class="w3-bar-item w3-button w3-ripple" <button
@click=${this.toggle_stay_connected} class="w3-bar-item w3-button w3-ripple"
> @click=${this.toggle_stay_connected}
${this.stay_connected ? '🔗' : '⛓️‍💥'} >
</button> ${this.stay_connected ? '🔗' : '⛓️‍💥'}
` </button>
: undefined} `
: undefined
}
${Object.entries(k_tabs).map( ${Object.entries(k_tabs).map(
([k, v]) => html` ([k, v]) => html`
<button <button
@@ -814,6 +825,8 @@ class TfElement extends LitElement {
> >
🎨<span class="w3-hide-small">Color</span> 🎨<span class="w3-hide-small">Color</span>
</button> </button>
<button class="w3-bar-item w3-button w3-right" @click=${this.search}>Search</button>
<input type="text" class="w3-input w3-bar-item w3-right w3-theme-d1" placeholder="keywords, @id, #channel" id="search_text" @keydown=${this.search_keydown}></input>
</div> </div>
`; `;
let contents = this.guest let contents = this.guest

View File

@@ -77,12 +77,6 @@ class TfTabSearchElement extends LitElement {
} }
} }
search_keydown(event) {
if (event.keyCode == 13) {
this.query = this.renderRoot.getElementById('search').value;
}
}
on_expand(event) { on_expand(event) {
if (event.detail.expanded) { if (event.detail.expanded) {
let expand = {}; let expand = {};
@@ -146,14 +140,10 @@ class TfTabSearchElement extends LitElement {
} }
let self = this; let self = this;
return html` return html`
<style>${generate_theme()}</style> <style>
<div class="w3-padding"> ${generate_theme()}
<div style="display: flex; flex-direction: row; gap: 4px"> </style>
<input type="text" class="w3-input w3-theme-d1" id="search" value=${this.query} style="flex: 1" @keydown=${this.search_keydown}></input> <div class="w3-padding">${this.render_results()}</div>
<button class="w3-button w3-theme-d1" @click=${(event) => self.search(self.renderRoot.getElementById('search').value)}>Search</button>
</div>
${this.render_results()}
</div>
`; `;
} }
} }

View File

@@ -1601,6 +1601,25 @@ function connectSocket(path) {
} }
} }
/**
* Determine a CodeMirror language mode from filename.
* @param name Filename.
* @return The mode name.
*/
function modeFromName(name) {
switch (name.split('.').pop()) {
case 'md':
return 'markdown';
case 'css':
return 'css';
case 'js':
return 'javascript';
case 'xml':
case 'svg':
return 'xml';
}
}
/** /**
* Open a file by name. * Open a file by name.
* @param name The file to open. * @param name The file to open.
@@ -1612,6 +1631,7 @@ function openFile(name) {
: cm6.EditorState.create({doc: '', extensions: cm6.extensions}); : cm6.EditorState.create({doc: '', extensions: cm6.extensions});
let oldDoc = g_editor.state; let oldDoc = g_editor.state;
g_editor.setState(newDoc); g_editor.setState(newDoc);
cm6.setEditorMode(g_editor, modeFromName(name));
if (g_files[g_current_file]) { if (g_files[g_current_file]) {
g_files[g_current_file].doc = oldDoc; g_files[g_current_file].doc = oldDoc;

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +1,14 @@
import {EditorState} from "@codemirror/state" import {EditorState, Compartment} from "@codemirror/state"
import {EditorView} from '@codemirror/view'; import {EditorView} from '@codemirror/view';
import {javascript} from "@codemirror/lang-javascript" import {javascript} from "@codemirror/lang-javascript"
import {html} from "@codemirror/lang-html" import {htmlLanguage, html} from "@codemirror/lang-html"
import {css} from "@codemirror/lang-css" import {css} from "@codemirror/lang-css"
import {markdown} from "@codemirror/lang-markdown"
import {xml} from "@codemirror/lang-xml"
import {search} from "@codemirror/search" import {search} from "@codemirror/search"
import {oneDark} from "./theme-tf-dark.js" import {oneDark} from "./theme-tf-dark.js"
import {lineNumbers, highlightActiveLineGutter, highlightSpecialChars, highlightTrailingWhitespace, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap, highlightWhitespace} from '@codemirror/view'; import {lineNumbers, highlightActiveLineGutter, highlightSpecialChars, highlightTrailingWhitespace, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine, keymap, highlightWhitespace} from '@codemirror/view';
import {foldGutter, indentUnit, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap} from '@codemirror/language'; import {language, foldGutter, indentUnit, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap} from '@codemirror/language';
import {history, defaultKeymap, historyKeymap, indentWithTab} from '@codemirror/commands'; import {history, defaultKeymap, historyKeymap, indentWithTab} from '@codemirror/commands';
import {highlightSelectionMatches, searchKeymap} from '@codemirror/search'; import {highlightSelectionMatches, searchKeymap} from '@codemirror/search';
import {autocompletion, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete'; import {autocompletion, closeBracketsKeymap, completionKeymap} from '@codemirror/autocomplete';
@@ -18,6 +20,23 @@ let updateListenerExtension = EditorView.updateListener.of((update) => {
} }
}); });
/* https://codemirror.net/examples/config/ */
const languageConfig = new Compartment();
const autoLanguage = EditorState.transactionExtender.of(tr => {
if (!tr.docChanged) {
return null;
}
let doc_is_html = /\s*</.test(tr.newDoc.sliceString(0, 100));
let state_is_html = tr.startState.facet(language) == htmlLanguage;
if (doc_is_html == state_is_html) {
return null;
}
return {
effects: languageConfig.reconfigure(doc_is_html ? html() : javascript()),
};
});
const extensions = [ const extensions = [
lineNumbers(), lineNumbers(),
highlightActiveLineGutter(), highlightActiveLineGutter(),
@@ -47,9 +66,8 @@ const extensions = [
...lintKeymap, ...lintKeymap,
indentWithTab, indentWithTab,
]), ]),
javascript(), languageConfig.of(javascript()),
html(), autoLanguage,
css(),
search(), search(),
oneDark, oneDark,
updateListenerExtension, updateListenerExtension,
@@ -60,11 +78,25 @@ function TildeFriendsEditorView(parent) {
extensions: extensions, extensions: extensions,
parent: parent, parent: parent,
}); });
}; }
function setEditorMode(view, mode) {
const k_modes = {
'css': css(),
'html': html(),
'javascript': javascript(),
'markdown': markdown(),
'xml': xml(),
};
view.dispatch({
effects: languageConfig.reconfigure(k_modes[mode])
});
}
export { export {
TildeFriendsEditorView, TildeFriendsEditorView,
EditorState, EditorState,
EditorView, EditorView,
extensions, extensions,
setEditorMode,
}; };

View File

@@ -9,6 +9,8 @@
"@codemirror/lang-html": "^6.4.8", "@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.0.1",
"@codemirror/lang-xml": "^6.0.1",
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
@@ -97,6 +99,35 @@
"@lezer/json": "^1.0.0" "@lezer/json": "^1.0.0"
} }
}, },
"node_modules/@codemirror/lang-markdown": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz",
"integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.3.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/markdown": "^1.0.0"
}
},
"node_modules/@codemirror/lang-xml": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
"integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/xml": "^1.0.0"
}
},
"node_modules/@codemirror/language": { "node_modules/@codemirror/language": {
"version": "6.11.3", "version": "6.11.3",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
@@ -284,6 +315,27 @@
"@lezer/common": "^1.0.0" "@lezer/common": "^1.0.0"
} }
}, },
"node_modules/@lezer/markdown": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.0.tgz",
"integrity": "sha512-AXb98u3M6BEzTnreBnGtQaF7xFTiMA92Dsy5tqEjpacbjRxDSFdN4bKJo9uvU4cEEOS7D2B9MT7kvDgOEIzJSw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@lezer/xml": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
"integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@marijn/find-cluster-break": { "node_modules/@marijn/find-cluster-break": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",

View File

@@ -7,6 +7,8 @@
"@codemirror/lang-html": "^6.4.8", "@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.0.1",
"@codemirror/lang-xml": "^6.0.1",
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",