@tnotesjs/core 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +0 -1
  2. package/config/defaultConfig.ts +33 -4
  3. package/config/index.ts +6 -0
  4. package/core/NoteIndexCache.ts +7 -1
  5. package/core/NoteManager.ts +18 -30
  6. package/dist/{chunk-5Y5IYS6C.js → chunk-UJG3UU5X.js} +21 -5
  7. package/dist/{chunk-5QN44ES5.js → chunk-XCWFZLER.js} +48 -368
  8. package/dist/cli/index.js +26 -78
  9. package/dist/index.js +1 -1
  10. package/dist/vitepress/config/index.js +2 -2
  11. package/package.json +1 -1
  12. package/services/index.ts +0 -1
  13. package/services/init-sub-repo/initSubRepoLogic.ts +0 -1
  14. package/services/note/service.ts +0 -1
  15. package/types/config.ts +0 -3
  16. package/types/note.ts +0 -2
  17. package/vitepress/assets/icons/icon__cursor.svg +4 -0
  18. package/vitepress/assets/icons/index.ts +1 -0
  19. package/vitepress/components/Layout/AboutPanel.vue +0 -24
  20. package/vitepress/components/Layout/DocBeforeControls.vue +6 -14
  21. package/vitepress/components/Layout/DocFooter.vue +3 -3
  22. package/vitepress/components/Layout/Layout.vue +0 -30
  23. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +20 -11
  24. package/vitepress/components/Layout/homeReadme.data.ts +0 -48
  25. package/vitepress/components/MarkMap/MarkMap.vue +6 -13
  26. package/vitepress/components/Settings/Settings.vue +76 -73
  27. package/vitepress/components/SidebarCard/FolderTreeItems.vue +16 -6
  28. package/vitepress/components/SidebarCard/SidebarCard.vue +21 -27
  29. package/vitepress/components/composables/useLocalIde.ts +83 -0
  30. package/vitepress/components/constants.ts +5 -6
  31. package/vitepress/components/utils/vscodePaths.test.ts +22 -0
  32. package/vitepress/components/utils/vscodePaths.ts +24 -2
  33. package/vitepress/theme/styles/doc-layout.scss +4 -4
  34. package/services/timestamp/index.ts +0 -7
  35. package/services/timestamp/service.ts +0 -465
@@ -12,17 +12,19 @@ import { onContentUpdated } from 'vitepress'
12
12
  import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
13
13
 
14
14
  import { icon__fullscreen, icon__fullscreen_exit, icon__confirm } from '../../assets/icons'
15
- import { MARKMAP_THEME_KEY, MARKMAP_EXPAND_LEVEL_KEY } from '../constants'
15
+ import { MARKMAP_THEME_KEY } from '../constants'
16
16
 
17
17
  // doc: https://github.com/markmap/markmap/blob/205367a24603dc187f67da1658940c6cade20dce/packages/markmap-view/src/constants.ts#L15
18
18
 
19
+ const DEFAULT_EXPAND_LEVEL = 3
20
+
19
21
  const props = defineProps({
20
22
  content: { type: String, default: '' },
21
23
  duration: { type: Number, default: 100 },
22
24
  spacingVertical: { type: Number, default: 10 },
23
25
  spacingHorizontal: { type: Number, default: 20 },
24
26
  nodeMinHeight: { type: Number, default: 24 },
25
- initialExpandLevel: { type: Number, default: 5 },
27
+ initialExpandLevel: { type: Number, default: DEFAULT_EXPAND_LEVEL },
26
28
  })
27
29
 
28
30
  const svgRef = ref<SVGSVGElement | null>(null)
@@ -36,15 +38,6 @@ let darkClassObserver: MutationObserver | null = null
36
38
 
37
39
  const isMarkmapDark = ref(false)
38
40
 
39
- // 从 localStorage 读取配置,如果没有则使用 props 或默认值
40
- const getInitialExpandLevel = () => {
41
- if (typeof window !== 'undefined') {
42
- const saved = localStorage.getItem(MARKMAP_EXPAND_LEVEL_KEY)
43
- if (saved) return parseInt(saved)
44
- }
45
- return props.initialExpandLevel
46
- }
47
-
48
41
  const getThemeColorFn = () => {
49
42
  if (typeof window !== 'undefined') {
50
43
  if (document.documentElement.classList.contains('markmap-dark')) {
@@ -63,8 +56,8 @@ const getThemeColorFn = () => {
63
56
  return scaleOrdinal(schemePastel2)
64
57
  }
65
58
 
66
- // 可由配置/props 初始化,也可以通过工具栏改动
67
- const expandLevel = ref(getInitialExpandLevel())
59
+ // 可由 props 初始化,也可以通过工具栏改动
60
+ const expandLevel = ref(props.initialExpandLevel)
68
61
  const transformer = new Transformer()
69
62
  const isFullscreen = ref(false)
70
63
 
@@ -10,33 +10,60 @@ vitepress/components/Settings/Settings.vue
10
10
  <div class="settingItem">
11
11
  <div class="settingMeta">
12
12
  <div class="labelLine">
13
- <span class="itemName">本地知识库路径</span>
13
+ <span class="itemName">本地 IDE 打开</span>
14
14
  <span class="infoWrap">
15
15
  <span class="infoIcon">?</span>
16
16
  <span class="tooltip">
17
- 适用于 PC 桌面环境,需要本地安装 VS Code。配置后可从页面快速打开本地笔记目录。
17
+ 适用于 PC 桌面环境。配置本地知识库 notes 目录路径,并选择 VS Code 或 Cursor,即可从页面快速打开知识库或笔记。
18
18
  </span>
19
19
  </span>
20
20
  </div>
21
21
  <span v-if="path" class="statusText">已配置</span>
22
22
  </div>
23
23
 
24
- <div class="inputGroup">
25
- <input
26
- v-model="path"
27
- type="text"
28
- placeholder="/Users/username/notes"
29
- class="input"
30
- />
31
- <button
32
- v-if="path"
33
- @click="clearPath"
34
- class="clearBtn"
35
- title="清空"
36
- type="button"
37
- >
38
- ×
39
- </button>
24
+ <div class="fieldStack">
25
+ <div class="controlRow">
26
+ <span class="controlLabel">IDE</span>
27
+ <div class="segmented segmented-two">
28
+ <label
29
+ v-for="option in localIdeOptions"
30
+ :key="option.value"
31
+ :class="[
32
+ 'segment',
33
+ { activeSegment: localIde === option.value },
34
+ ]"
35
+ >
36
+ <input
37
+ v-model="localIde"
38
+ type="radio"
39
+ name="local-ide"
40
+ :value="option.value"
41
+ />
42
+ <span>{{ option.label }}</span>
43
+ </label>
44
+ </div>
45
+ </div>
46
+
47
+ <label class="field">
48
+ <span class="controlLabel">知识库路径</span>
49
+ <div class="inputGroup">
50
+ <input
51
+ v-model="path"
52
+ type="text"
53
+ placeholder="/Users/username/notes"
54
+ class="input pathInput"
55
+ />
56
+ <button
57
+ v-if="path"
58
+ @click="clearPath"
59
+ class="clearBtn"
60
+ title="清空"
61
+ type="button"
62
+ >
63
+ ×
64
+ </button>
65
+ </div>
66
+ </label>
40
67
  </div>
41
68
  </div>
42
69
 
@@ -128,36 +155,6 @@ vitepress/components/Settings/Settings.vue
128
155
  </div>
129
156
  </div>
130
157
  </div>
131
-
132
- <div class="settingItem">
133
- <div class="settingMeta">
134
- <div class="labelLine">
135
- <span class="itemName">笔记内 MarkMap</span>
136
- <span class="infoWrap">
137
- <span class="infoIcon">?</span>
138
- <span class="tooltip">
139
- 配置笔记中 markmap 代码块的初始展开层级。
140
- </span>
141
- </span>
142
- </div>
143
- </div>
144
-
145
- <div class="inlineGrid">
146
- <label class="field">
147
- <span class="controlLabel">展开层级</span>
148
- <div class="inputWithUnit">
149
- <input
150
- v-model.number="markmapExpandLevel"
151
- type="number"
152
- min="1"
153
- max="100"
154
- class="input"
155
- />
156
- <span class="unit">层</span>
157
- </div>
158
- </label>
159
- </div>
160
- </div>
161
158
  </section>
162
159
 
163
160
  <div class="actionBar">
@@ -185,12 +182,18 @@ import { ref, computed, onMounted } from 'vue'
185
182
 
186
183
  import {
187
184
  NOTES_DIR_KEY,
188
- MARKMAP_EXPAND_LEVEL_KEY,
185
+ LOCAL_IDE_KEY,
189
186
  SIDEBAR_SHOW_NOTE_ID_KEY,
190
187
  SIDEBAR_DONE_PREFIX_KEY,
191
188
  SIDEBAR_UNDONE_PREFIX_KEY,
192
189
  CONTENT_WIDTH_MODE_KEY,
193
190
  } from '../constants'
191
+ import { useLocalIde } from '../composables/useLocalIde'
192
+ import {
193
+ DEFAULT_LOCAL_IDE,
194
+ normalizeLocalIde,
195
+ type LocalIdeId,
196
+ } from '../utils/vscodePaths'
194
197
  // @ts-expect-error - VitePress Data Loader
195
198
  import { data as tnotesConfig } from '../tnotes-config.data'
196
199
 
@@ -200,6 +203,8 @@ const DEFAULT_CONTENT_WIDTH_MODE: ContentWidthMode = 'wide'
200
203
  const DEFAULT_DONE_PREFIX = '✅'
201
204
  const DEFAULT_UNDONE_PREFIX = '⏰'
202
205
 
206
+ const { setLocalIde } = useLocalIde()
207
+
203
208
  const contentWidthModeOptions: Array<{
204
209
  label: string
205
210
  value: ContentWidthMode
@@ -208,10 +213,15 @@ const contentWidthModeOptions: Array<{
208
213
  { label: '标准页宽', value: 'standard' },
209
214
  ]
210
215
 
216
+ const localIdeOptions: Array<{ label: string; value: LocalIdeId }> = [
217
+ { label: 'VS Code', value: 'vscode' },
218
+ { label: 'Cursor', value: 'cursor' },
219
+ ]
220
+
211
221
  const path = ref('')
212
222
  const originalPath = ref('')
213
- const markmapExpandLevel = ref(5)
214
- const originalMarkmapExpandLevel = ref(5)
223
+ const localIde = ref<LocalIdeId>(DEFAULT_LOCAL_IDE)
224
+ const originalLocalIde = ref<LocalIdeId>(DEFAULT_LOCAL_IDE)
215
225
  const showNoteId = ref(false)
216
226
  const originalShowNoteId = ref(false)
217
227
  const donePrefix = ref(DEFAULT_DONE_PREFIX)
@@ -227,7 +237,7 @@ const showSuccessToast = ref(false)
227
237
  const hasChanges = computed(
228
238
  () =>
229
239
  path.value !== originalPath.value ||
230
- markmapExpandLevel.value !== originalMarkmapExpandLevel.value ||
240
+ localIde.value !== originalLocalIde.value ||
231
241
  showNoteId.value !== originalShowNoteId.value ||
232
242
  donePrefix.value !== originalDonePrefix.value ||
233
243
  undonePrefix.value !== originalUndonePrefix.value ||
@@ -246,9 +256,9 @@ onMounted(() => {
246
256
  path.value = savedPath
247
257
  originalPath.value = savedPath
248
258
 
249
- const savedLevel = localStorage.getItem(MARKMAP_EXPAND_LEVEL_KEY) || '5'
250
- markmapExpandLevel.value = parseInt(savedLevel, 10)
251
- originalMarkmapExpandLevel.value = markmapExpandLevel.value
259
+ const savedLocalIde = normalizeLocalIde(localStorage.getItem(LOCAL_IDE_KEY))
260
+ localIde.value = savedLocalIde
261
+ originalLocalIde.value = savedLocalIde
252
262
 
253
263
  const savedShowNoteId = localStorage.getItem(SIDEBAR_SHOW_NOTE_ID_KEY)
254
264
  showNoteId.value =
@@ -293,17 +303,14 @@ function save() {
293
303
  contentWidthMode.value !== originalContentWidthMode.value
294
304
 
295
305
  localStorage.setItem(NOTES_DIR_KEY, path.value)
296
- localStorage.setItem(
297
- MARKMAP_EXPAND_LEVEL_KEY,
298
- markmapExpandLevel.value.toString(),
299
- )
306
+ setLocalIde(localIde.value)
300
307
  localStorage.setItem(SIDEBAR_SHOW_NOTE_ID_KEY, showNoteId.value.toString())
301
308
  localStorage.setItem(SIDEBAR_DONE_PREFIX_KEY, donePrefix.value)
302
309
  localStorage.setItem(SIDEBAR_UNDONE_PREFIX_KEY, undonePrefix.value)
303
310
  localStorage.setItem(CONTENT_WIDTH_MODE_KEY, contentWidthMode.value)
304
311
 
305
312
  originalPath.value = path.value
306
- originalMarkmapExpandLevel.value = markmapExpandLevel.value
313
+ originalLocalIde.value = localIde.value
307
314
  originalShowNoteId.value = showNoteId.value
308
315
  originalDonePrefix.value = donePrefix.value
309
316
  originalUndonePrefix.value = undonePrefix.value
@@ -327,7 +334,7 @@ function save() {
327
334
 
328
335
  function reset() {
329
336
  path.value = originalPath.value
330
- markmapExpandLevel.value = originalMarkmapExpandLevel.value
337
+ localIde.value = originalLocalIde.value
331
338
  showNoteId.value = originalShowNoteId.value
332
339
  donePrefix.value = originalDonePrefix.value
333
340
  undonePrefix.value = originalUndonePrefix.value
@@ -400,10 +407,12 @@ function reset() {
400
407
 
401
408
  .tooltip {
402
409
  position: absolute;
403
- left: 50%;
404
- bottom: calc(100% + 8px);
405
- z-index: 10;
410
+ left: 0;
411
+ top: calc(100% + 8px);
412
+ bottom: auto;
413
+ z-index: 20;
406
414
  width: 240px;
415
+ max-width: min(240px, calc(100vw - 48px));
407
416
  padding: 8px 10px;
408
417
  color: var(--vp-c-text-1);
409
418
  background: var(--vp-c-bg-elv);
@@ -414,7 +423,7 @@ function reset() {
414
423
  line-height: 18px;
415
424
  opacity: 0;
416
425
  pointer-events: none;
417
- transform: translate(-50%, 4px);
426
+ transform: translateY(-4px);
418
427
  transition:
419
428
  opacity 0.15s ease,
420
429
  transform 0.15s ease;
@@ -423,7 +432,7 @@ function reset() {
423
432
  .infoWrap:hover .tooltip,
424
433
  .infoWrap:focus-within .tooltip {
425
434
  opacity: 1;
426
- transform: translate(-50%, 0);
435
+ transform: translateY(0);
427
436
  }
428
437
 
429
438
  .inputGroup {
@@ -699,14 +708,8 @@ function reset() {
699
708
  }
700
709
 
701
710
  .tooltip {
702
- left: 0;
703
711
  width: min(260px, calc(100vw - 48px));
704
- transform: translate(0, 4px);
705
- }
706
-
707
- .infoWrap:hover .tooltip,
708
- .infoWrap:focus-within .tooltip {
709
- transform: translate(0, 0);
712
+ max-width: min(260px, calc(100vw - 48px));
710
713
  }
711
714
 
712
715
  .actionBar {
@@ -6,8 +6,8 @@ vitepress/components/SidebarCard/FolderTreeItems.vue
6
6
  import {
7
7
  icon__sidebar_collapsed,
8
8
  icon__sidebar_opened,
9
- icon__vscode,
10
9
  } from '../../assets/icons'
10
+ import { useLocalIde } from '../composables/useLocalIde'
11
11
 
12
12
  const props = defineProps({
13
13
  nodes: {
@@ -30,6 +30,8 @@ const props = defineProps({
30
30
 
31
31
  const emit = defineEmits(['toggle-node', 'open-vscode'])
32
32
 
33
+ const { icon: localIdeIcon, openNoteTitle } = useLocalIde()
34
+
33
35
  function hasChildren(node) {
34
36
  return node.children?.length > 0
35
37
  }
@@ -109,21 +111,24 @@ function onOpenVscode(node, event) {
109
111
  <span class="tree-title">{{ getDisplayTitle(node) }}</span>
110
112
  </a>
111
113
 
112
- <span
114
+ <button
113
115
  v-else-if="hasChildren(node)"
116
+ type="button"
114
117
  class="tree-link tree-link-folder"
118
+ :title="isExpanded(node) ? '折叠分组' : '展开分组'"
119
+ @click="onToggle(node)"
115
120
  >
116
121
  <span class="tree-title">{{ getDisplayTitle(node) }}</span>
117
- </span>
122
+ </button>
118
123
 
119
124
  <button
120
125
  v-if="node.relativePath"
121
126
  class="vscode-article"
122
127
  type="button"
123
- title="在 VS Code 中打开笔记目录"
128
+ :title="openNoteTitle"
124
129
  @click="onOpenVscode(node, $event)"
125
130
  >
126
- <img :src="icon__vscode" alt="打开笔记目录" />
131
+ <img :src="localIdeIcon" :alt="openNoteTitle" />
127
132
  </button>
128
133
  </div>
129
134
 
@@ -225,9 +230,14 @@ export default {
225
230
  }
226
231
 
227
232
  .tree-link-folder {
228
- cursor: default;
233
+ cursor: pointer;
229
234
  font-weight: 600;
230
235
  color: var(--vp-c-text-1);
236
+ border: none;
237
+ background: none;
238
+ padding: 0;
239
+ text-align: left;
240
+ font: inherit;
231
241
  }
232
242
 
233
243
  .tree-status {
@@ -9,13 +9,13 @@ import { ref, computed, watch } from 'vue'
9
9
  import {
10
10
  icon__fold,
11
11
  icon__github,
12
- icon__vscode,
13
12
  icon__folder,
14
13
  icon__search,
15
14
  icon__number_gray,
16
15
  icon__number_purple,
17
16
  } from '../../assets/icons'
18
17
  import { NOTES_DIR_KEY, REPO_NAME, AUTHOR, ROOT_ITEM } from '../constants.ts'
18
+ import { useLocalIde } from '../composables/useLocalIde'
19
19
  import FolderTreeItems from './FolderTreeItems.vue'
20
20
  import NotesTrendChart from './NotesTrendChart.vue'
21
21
  import { useSettingsDialog } from '../Settings/composables/useSettingsDialog'
@@ -29,9 +29,7 @@ import {
29
29
  import {
30
30
  getRepoRootPath,
31
31
  resolveNoteReadmePath,
32
- toVscodeFileUrl,
33
32
  } from '../utils/vscodePaths'
34
- import { formatDate } from '../utils.ts'
35
33
 
36
34
  // #region props
37
35
  const props = defineProps({
@@ -46,6 +44,14 @@ const props = defineProps({
46
44
  })
47
45
  // #endregion
48
46
 
47
+ const {
48
+ icon: localIdeIcon,
49
+ openNoteTitle,
50
+ openRepoTitle,
51
+ openReadmeTitle,
52
+ toFileUrl,
53
+ } = useLocalIde()
54
+
49
55
  // #region data
50
56
  const viewMode = ref('folder')
51
57
  const expandedTreeKeys = ref(new Set())
@@ -217,7 +223,7 @@ function openGithubRepo() {
217
223
  window.open(`https://github.com/${AUTHOR}/${REPO_NAME}`, '_blank')
218
224
  }
219
225
 
220
- // 打开 VS Code 知识库
226
+ // 打开本地 IDE 知识库
221
227
  function openVSCodeRepo() {
222
228
  const notesDir = localStorage.getItem(NOTES_DIR_KEY)
223
229
 
@@ -231,10 +237,10 @@ function openVSCodeRepo() {
231
237
  return
232
238
  }
233
239
 
234
- window.open('vscode://file/' + notesDir, '_blank')
240
+ window.open(toFileUrl(notesDir), '_blank')
235
241
  }
236
242
 
237
- // 打开 VS Code 中的笔记 README.md
243
+ // 在本地 IDE 中打开笔记 README.md
238
244
  function openVSCodeArticle(article) {
239
245
  const notesDir = localStorage.getItem(NOTES_DIR_KEY)
240
246
 
@@ -251,7 +257,7 @@ function openVSCodeArticle(article) {
251
257
  const notePath = resolveNoteReadmePath(notesDir, article.relativePath)
252
258
  if (!notePath) return
253
259
 
254
- window.open(toVscodeFileUrl(notePath), '_blank')
260
+ window.open(toFileUrl(notePath), '_blank')
255
261
  }
256
262
 
257
263
  function openVSCodeReadme() {
@@ -268,7 +274,7 @@ function openVSCodeReadme() {
268
274
  }
269
275
 
270
276
  const readmePath = `${getRepoRootPath(notesDir)}/README.md`
271
- window.open(toVscodeFileUrl(readmePath), '_blank')
277
+ window.open(toFileUrl(readmePath), '_blank')
272
278
  }
273
279
  </script>
274
280
 
@@ -318,9 +324,9 @@ function openVSCodeReadme() {
318
324
  <img :src="icon__github" alt="GitHub仓库" />
319
325
  </button>
320
326
 
321
- <!-- VS Code 按钮 -->
322
- <button class="vscode-open" @click="openVSCodeRepo">
323
- <img :src="icon__vscode" alt="使用 VS Code 打开本地知识库" />
327
+ <!-- 本地 IDE 按钮 -->
328
+ <button class="vscode-open" @click="openVSCodeRepo" :title="openRepoTitle">
329
+ <img :src="localIdeIcon" :alt="openRepoTitle" />
324
330
  </button>
325
331
 
326
332
  <!-- 关于按钮 -->
@@ -346,18 +352,6 @@ function openVSCodeReadme() {
346
352
  {{ AUTHOR }}/{{ REPO_NAME }}
347
353
  </a>
348
354
  </div>
349
- <div class="about-item">
350
- <span class="about-label">📅 创建时间</span>
351
- <span class="about-value">{{
352
- formatDate(ROOT_ITEM.created_at)
353
- }}</span>
354
- </div>
355
- <div class="about-item">
356
- <span class="about-label">♻️ 最近更新</span>
357
- <span class="about-value">{{
358
- formatDate(ROOT_ITEM.updated_at)
359
- }}</span>
360
- </div>
361
355
  </div>
362
356
  </div>
363
357
  </div>
@@ -395,9 +389,9 @@ function openVSCodeReadme() {
395
389
  <button
396
390
  class="vscode-article"
397
391
  @click.stop="openVSCodeArticle(article)"
398
- title="在 VS Code 中打开笔记目录"
392
+ :title="openNoteTitle"
399
393
  >
400
- <img :src="icon__vscode" alt="打开笔记目录" />
394
+ <img :src="localIdeIcon" :alt="openNoteTitle" />
401
395
  </button>
402
396
  </div>
403
397
  </div>
@@ -415,10 +409,10 @@ function openVSCodeReadme() {
415
409
  <button
416
410
  class="vscode-article"
417
411
  type="button"
418
- title="在 VS Code 中打开 README.md"
412
+ :title="openReadmeTitle"
419
413
  @click="openVSCodeReadme"
420
414
  >
421
- <img :src="icon__vscode" alt="打开 README.md" />
415
+ <img :src="localIdeIcon" :alt="openReadmeTitle" />
422
416
  </button>
423
417
  </div>
424
418
  <FolderTreeItems
@@ -0,0 +1,83 @@
1
+ /**
2
+ * vitepress/components/composables/useLocalIde.ts
3
+ *
4
+ * 本地 IDE(VS Code / Cursor)选择状态与展示信息
5
+ */
6
+
7
+ import { computed, ref } from 'vue'
8
+
9
+ import { icon__cursor, icon__vscode } from '../../assets/icons'
10
+ import { LOCAL_IDE_KEY } from '../constants'
11
+ import {
12
+ DEFAULT_LOCAL_IDE,
13
+ normalizeLocalIde,
14
+ toIdeFileUrl,
15
+ type LocalIdeId,
16
+ } from '../utils/vscodePaths'
17
+
18
+ const ide = ref<LocalIdeId>(DEFAULT_LOCAL_IDE)
19
+ let hydrated = false
20
+
21
+ function readIdeFromStorage(): LocalIdeId {
22
+ if (typeof window === 'undefined') return DEFAULT_LOCAL_IDE
23
+ return normalizeLocalIde(localStorage.getItem(LOCAL_IDE_KEY))
24
+ }
25
+
26
+ function hydrateFromStorage() {
27
+ if (typeof window === 'undefined' || hydrated) return
28
+ ide.value = readIdeFromStorage()
29
+ hydrated = true
30
+ }
31
+
32
+ export function useLocalIde() {
33
+ hydrateFromStorage()
34
+
35
+ const icon = computed(() =>
36
+ ide.value === 'cursor' ? icon__cursor : icon__vscode,
37
+ )
38
+
39
+ const shortLabel = computed(() =>
40
+ ide.value === 'cursor' ? 'Cursor' : 'VS Code',
41
+ )
42
+
43
+ const openNoteTitle = computed(
44
+ () => `用 ${shortLabel.value} 打开笔记目录`,
45
+ )
46
+ const openRepoTitle = computed(
47
+ () => `用 ${shortLabel.value} 打开本地知识库`,
48
+ )
49
+ const openReadmeTitle = computed(
50
+ () => `用 ${shortLabel.value} 打开 README.md`,
51
+ )
52
+ const openCurrentTitle = computed(
53
+ () => `用 ${shortLabel.value} 打开当前笔记`,
54
+ )
55
+
56
+ function setLocalIde(next: LocalIdeId) {
57
+ ide.value = normalizeLocalIde(next)
58
+ if (typeof window !== 'undefined') {
59
+ localStorage.setItem(LOCAL_IDE_KEY, ide.value)
60
+ }
61
+ }
62
+
63
+ function refreshLocalIde() {
64
+ ide.value = readIdeFromStorage()
65
+ }
66
+
67
+ function toFileUrl(filePath: string) {
68
+ return toIdeFileUrl(filePath, ide.value)
69
+ }
70
+
71
+ return {
72
+ ide,
73
+ icon,
74
+ shortLabel,
75
+ openNoteTitle,
76
+ openRepoTitle,
77
+ openReadmeTitle,
78
+ openCurrentTitle,
79
+ setLocalIde,
80
+ refreshLocalIde,
81
+ toFileUrl,
82
+ }
83
+ }
@@ -37,6 +37,11 @@ export const ROOT_ITEM = __TNOTES_ROOT_ITEM__
37
37
  */
38
38
  export const NOTES_DIR_KEY: string = 'NOTES_DIR_KEY__' + REPO_NAME
39
39
 
40
+ /**
41
+ * 本地打开所用 IDE(vscode | cursor)
42
+ */
43
+ export const LOCAL_IDE_KEY: string = 'LOCAL_IDE_KEY__' + REPO_NAME
44
+
40
45
  /**
41
46
  * 用户选择的视图
42
47
  */
@@ -53,12 +58,6 @@ export const EN_WORD_LIST_COMP_IS_AUTO_SHOW_CARD: string =
53
58
  */
54
59
  export const MARKMAP_THEME_KEY: string = 'MARKMAP_THEME_KEY__' + REPO_NAME
55
60
 
56
- /**
57
- * MarkMap 默认展开层级配置
58
- */
59
- export const MARKMAP_EXPAND_LEVEL_KEY: string =
60
- 'MARKMAP_EXPAND_LEVEL_KEY__' + REPO_NAME
61
-
62
61
  /**
63
62
  * 侧边栏是否显示笔记编号配置
64
63
  */
@@ -2,7 +2,9 @@ import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import {
4
4
  getRepoRootPath,
5
+ normalizeLocalIde,
5
6
  resolveNoteReadmePath,
7
+ toIdeFileUrl,
6
8
  toVscodeFileUrl,
7
9
  } from './vscodePaths'
8
10
 
@@ -33,6 +35,26 @@ describe('vscodePaths', () => {
33
35
  )
34
36
  })
35
37
 
38
+ it('normalizeLocalIde falls back to vscode', () => {
39
+ expect(normalizeLocalIde(null)).toBe('vscode')
40
+ expect(normalizeLocalIde('')).toBe('vscode')
41
+ expect(normalizeLocalIde('foo')).toBe('vscode')
42
+ expect(normalizeLocalIde('cursor')).toBe('cursor')
43
+ expect(normalizeLocalIde('vscode')).toBe('vscode')
44
+ })
45
+
46
+ it('toIdeFileUrl uses vscode scheme by default', () => {
47
+ expect(toIdeFileUrl('C:\\notes\\0001\\README.md')).toContain(
48
+ 'vscode://file/',
49
+ )
50
+ })
51
+
52
+ it('toIdeFileUrl uses cursor scheme when selected', () => {
53
+ expect(toIdeFileUrl('C:\\notes\\0001\\README.md', 'cursor')).toContain(
54
+ 'cursor://file/',
55
+ )
56
+ })
57
+
36
58
  it('toVscodeFileUrl encodes file path', () => {
37
59
  expect(toVscodeFileUrl('C:\\notes\\0001\\README.md')).toContain(
38
60
  'vscode://file/',