mirror of
https://github.com/RWejlgaard/org.git
synced 2026-05-06 04:34:45 +00:00
feat: adding multi-file mode to allow opening all org files in a directory (#10)
This commit is contained in:
parent
5eb672e0c9
commit
2e9980e73c
7 changed files with 324 additions and 31 deletions
43
README.md
43
README.md
|
|
@ -18,9 +18,46 @@ go build -o bin/org ./cmd/org
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
org [filename] # Open specific org file
|
org # Open ./todo.org (default)
|
||||||
org -f tasks.org # Open using -f flag
|
org tasks.org # Open specific org file
|
||||||
org # Opens ./todo.org by default
|
org /path/to/work.org # Open specific org file with path
|
||||||
|
org -m # Multi-file: Load all .org files in current directory
|
||||||
|
org -m /path/to/dir # Multi-file: Load all .org files in specified directory
|
||||||
|
```
|
||||||
|
|
||||||
|
### Single-File Mode (Default)
|
||||||
|
|
||||||
|
By default, `org` opens `./todo.org` or the file you specify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
org # Opens ./todo.org
|
||||||
|
org tasks.org # Opens tasks.org
|
||||||
|
org ~/work/project.org # Opens specific file
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-File Mode
|
||||||
|
|
||||||
|
Use the `-m` or `--multi` flag to load all `.org` files in a directory as top-level items. Each file appears as a top-level item in the interface, with its contents nested underneath. Changes made to items are automatically saved back to their respective files.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
org -m # Load all .org files in current directory
|
||||||
|
org -m /path/to/dir # Load all .org files in specified directory
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:** If you have these files in your directory:
|
||||||
|
- `work.org` containing work tasks
|
||||||
|
- `personal.org` containing personal tasks
|
||||||
|
- `ideas.org` containing project ideas
|
||||||
|
|
||||||
|
Running `org -m` will display:
|
||||||
|
```
|
||||||
|
* work.org
|
||||||
|
** TODO Complete project proposal
|
||||||
|
** PROG Review code changes
|
||||||
|
* personal.org
|
||||||
|
** TODO Buy groceries
|
||||||
|
* ideas.org
|
||||||
|
** New app concept
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
|
||||||
|
|
@ -7,31 +7,23 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/rwejlgaard/org/internal/config"
|
"github.com/rwejlgaard/org/internal/config"
|
||||||
|
"github.com/rwejlgaard/org/internal/model"
|
||||||
"github.com/rwejlgaard/org/internal/parser"
|
"github.com/rwejlgaard/org/internal/parser"
|
||||||
"github.com/rwejlgaard/org/internal/ui"
|
"github.com/rwejlgaard/org/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var filePath string
|
var filePath string
|
||||||
flag.StringVar(&filePath, "file", "", "Path to org file (default: ./todo.org)")
|
var multiMode bool
|
||||||
flag.StringVar(&filePath, "f", "", "Path to org file (shorthand)")
|
flag.BoolVar(&multiMode, "multi", false, "Load all org files in current directory as top-level items")
|
||||||
|
flag.BoolVar(&multiMode, "m", false, "Load all org files in current directory (shorthand)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Check for positional argument first
|
// Check for positional argument
|
||||||
if filePath == "" && len(flag.Args()) > 0 {
|
if filePath == "" && len(flag.Args()) > 0 {
|
||||||
filePath = flag.Args()[0]
|
filePath = flag.Args()[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to ./todo.org if no file specified
|
|
||||||
if filePath == "" {
|
|
||||||
cwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error getting current directory: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
filePath = filepath.Join(cwd, "todo.org")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration
|
||||||
cfg, err := config.LoadConfig()
|
cfg, err := config.LoadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -39,11 +31,53 @@ func main() {
|
||||||
cfg = config.DefaultConfig()
|
cfg = config.DefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the org file
|
var orgFile *model.OrgFile
|
||||||
orgFile, err := parser.ParseOrgFile(filePath, cfg)
|
|
||||||
if err != nil {
|
if multiMode {
|
||||||
fmt.Fprintf(os.Stderr, "Error parsing org file: %v\n", err)
|
// Multi-file mode: load all .org files in directory
|
||||||
os.Exit(1)
|
var dirPath string
|
||||||
|
if filePath != "" {
|
||||||
|
// Check if provided path is a directory
|
||||||
|
info, err := os.Stat(filePath)
|
||||||
|
if err == nil && info.IsDir() {
|
||||||
|
dirPath = filePath
|
||||||
|
} else {
|
||||||
|
// Use directory of the provided file path
|
||||||
|
dirPath = filepath.Dir(filePath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use current directory
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error getting current directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
dirPath = cwd
|
||||||
|
}
|
||||||
|
|
||||||
|
orgFile, err = parser.ParseMultipleOrgFiles(dirPath, cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error parsing org files: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single file mode (default)
|
||||||
|
if filePath == "" {
|
||||||
|
// Default to ./todo.org
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error getting current directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
filePath = filepath.Join(cwd, "todo.org")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the org file
|
||||||
|
orgFile, err = parser.ParseOrgFile(filePath, cfg)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error parsing org file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the UI
|
// Run the UI
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ type Item struct {
|
||||||
Children []*Item // Sub-items
|
Children []*Item // Sub-items
|
||||||
Folded bool // Whether the item is folded (hides notes and children)
|
Folded bool // Whether the item is folded (hides notes and children)
|
||||||
ClockEntries []ClockEntry // Clock in/out entries
|
ClockEntries []ClockEntry // Clock in/out entries
|
||||||
|
SourceFile string // Source file path (used in multi-file mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrgFile represents a parsed org-mode file
|
// OrgFile represents a parsed org-mode file
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ package parser
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rwejlgaard/org/internal/config"
|
"github.com/rwejlgaard/org/internal/config"
|
||||||
|
|
@ -216,3 +218,70 @@ func ParseOrgFile(path string, cfg *config.Config) (*model.OrgFile, error) {
|
||||||
|
|
||||||
return orgFile, nil
|
return orgFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseMultipleOrgFiles loads all .org files in a directory and wraps them as top-level items
|
||||||
|
func ParseMultipleOrgFiles(dirPath string, cfg *config.Config) (*model.OrgFile, error) {
|
||||||
|
// Find all .org files in the directory
|
||||||
|
matches, err := filepath.Glob(filepath.Join(dirPath, "*.org"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort files alphabetically
|
||||||
|
sort.Strings(matches)
|
||||||
|
|
||||||
|
// Create a virtual org file
|
||||||
|
multiOrgFile := &model.OrgFile{
|
||||||
|
Path: dirPath, // Store directory path
|
||||||
|
Items: []*model.Item{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse each file and wrap it as a top-level item
|
||||||
|
for _, filePath := range matches {
|
||||||
|
orgFile, err := ParseOrgFile(filePath, cfg)
|
||||||
|
if err != nil {
|
||||||
|
// Skip files that can't be parsed
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a wrapper item for this file
|
||||||
|
fileName := filepath.Base(filePath)
|
||||||
|
fileItem := &model.Item{
|
||||||
|
Level: 1,
|
||||||
|
State: model.StateNone,
|
||||||
|
Priority: model.PriorityNone,
|
||||||
|
Title: fileName,
|
||||||
|
Tags: []string{},
|
||||||
|
Notes: []string{},
|
||||||
|
Children: []*model.Item{},
|
||||||
|
SourceFile: filePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the level of all items from this file and add as children
|
||||||
|
for _, item := range orgFile.Items {
|
||||||
|
incrementItemLevel(item)
|
||||||
|
setSourceFileRecursive(item, filePath)
|
||||||
|
fileItem.Children = append(fileItem.Children, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
multiOrgFile.Items = append(multiOrgFile.Items, fileItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return multiOrgFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// incrementItemLevel recursively increments the level of an item and its children
|
||||||
|
func incrementItemLevel(item *model.Item) {
|
||||||
|
item.Level++
|
||||||
|
for _, child := range item.Children {
|
||||||
|
incrementItemLevel(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSourceFileRecursive sets the source file for an item and all its descendants
|
||||||
|
func setSourceFileRecursive(item *model.Item, filePath string) {
|
||||||
|
item.SourceFile = filePath
|
||||||
|
for _, child := range item.Children {
|
||||||
|
setSourceFileRecursive(child, filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,18 @@ import (
|
||||||
|
|
||||||
// Save writes the org file back to disk
|
// Save writes the org file back to disk
|
||||||
func Save(orgFile *model.OrgFile) error {
|
func Save(orgFile *model.OrgFile) error {
|
||||||
|
// Check if this is a multi-file org (directory-based)
|
||||||
|
// In multi-file mode, top-level items have SourceFile set and represent files
|
||||||
|
isMultiFile := false
|
||||||
|
if len(orgFile.Items) > 0 && orgFile.Items[0].SourceFile != "" {
|
||||||
|
isMultiFile = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMultiFile {
|
||||||
|
return saveMultiFile(orgFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single file mode
|
||||||
file, err := os.Create(orgFile.Path)
|
file, err := os.Create(orgFile.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -29,6 +41,66 @@ func Save(orgFile *model.OrgFile) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// saveMultiFile saves items back to their individual source files
|
||||||
|
func saveMultiFile(orgFile *model.OrgFile) error {
|
||||||
|
// Group items by source file
|
||||||
|
fileItems := make(map[string][]*model.Item)
|
||||||
|
|
||||||
|
for _, fileItem := range orgFile.Items {
|
||||||
|
if fileItem.SourceFile == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// The children of this file item are the actual items to save
|
||||||
|
fileItems[fileItem.SourceFile] = fileItem.Children
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save each file
|
||||||
|
for filePath, items := range fileItems {
|
||||||
|
if err := saveItemsToFile(filePath, items); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveItemsToFile writes a list of items to a specific file
|
||||||
|
func saveItemsToFile(filePath string, items []*model.Item) error {
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
writer := bufio.NewWriter(file)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
// Decrement level since we're saving to individual files
|
||||||
|
decrementedItem := decrementItemLevelForSave(item)
|
||||||
|
if err := writeItem(writer, decrementedItem); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrementItemLevelForSave creates a copy of an item with decremented levels for saving
|
||||||
|
func decrementItemLevelForSave(item *model.Item) *model.Item {
|
||||||
|
copied := *item
|
||||||
|
copied.Level--
|
||||||
|
|
||||||
|
copiedChildren := make([]*model.Item, len(item.Children))
|
||||||
|
for i, child := range item.Children {
|
||||||
|
copiedChildren[i] = decrementItemLevelForSave(child)
|
||||||
|
}
|
||||||
|
copied.Children = copiedChildren
|
||||||
|
|
||||||
|
return &copied
|
||||||
|
}
|
||||||
|
|
||||||
// writeItem recursively writes an item and its children
|
// writeItem recursively writes an item and its children
|
||||||
func writeItem(writer *bufio.Writer, item *model.Item) error {
|
func writeItem(writer *bufio.Writer, item *model.Item) error {
|
||||||
// Write heading
|
// Write heading
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ type uiModel struct {
|
||||||
settingsCursor int // Cursor position in settings view
|
settingsCursor int // Cursor position in settings view
|
||||||
settingsScroll int // Scroll position in settings view
|
settingsScroll int // Scroll position in settings view
|
||||||
settingsSection settingsSection // Current settings section/tab
|
settingsSection settingsSection // Current settings section/tab
|
||||||
|
captureCursor int // Store cursor position when entering capture mode
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitialModel(orgFile *model.OrgFile, cfg *config.Config) uiModel {
|
func InitialModel(orgFile *model.OrgFile, cfg *config.Config) uiModel {
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,18 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case key.Matches(msg, m.keys.EditNotes):
|
case key.Matches(msg, m.keys.EditNotes):
|
||||||
items := m.getVisibleItems()
|
items := m.getVisibleItems()
|
||||||
if len(items) > 0 && m.cursor < len(items) {
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
m.editingItem = items[m.cursor]
|
selectedItem := items[m.cursor]
|
||||||
|
|
||||||
|
// Check if we're in multi-file mode
|
||||||
|
isMultiFile := len(m.orgFile.Items) > 0 && m.orgFile.Items[0].SourceFile != ""
|
||||||
|
|
||||||
|
// Prevent editing notes for top-level file items in multi-file mode
|
||||||
|
if isMultiFile && selectedItem.Level == 1 && selectedItem.SourceFile != "" {
|
||||||
|
m.setStatus("Cannot add notes to file-level items")
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.editingItem = selectedItem
|
||||||
m.mode = modeEdit
|
m.mode = modeEdit
|
||||||
m.textarea.SetValue(strings.Join(m.editingItem.Notes, "\n"))
|
m.textarea.SetValue(strings.Join(m.editingItem.Notes, "\n"))
|
||||||
m.textarea.Focus()
|
m.textarea.Focus()
|
||||||
|
|
@ -173,6 +184,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
case key.Matches(msg, m.keys.Capture):
|
case key.Matches(msg, m.keys.Capture):
|
||||||
m.mode = modeCapture
|
m.mode = modeCapture
|
||||||
|
m.captureCursor = m.cursor // Store current cursor position
|
||||||
m.textinput.SetValue("")
|
m.textinput.SetValue("")
|
||||||
m.textinput.Placeholder = "What needs doing?"
|
m.textinput.Placeholder = "What needs doing?"
|
||||||
m.textinput.Focus()
|
m.textinput.Focus()
|
||||||
|
|
@ -356,13 +368,36 @@ func (m uiModel) updateCapture(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
Notes: []string{},
|
Notes: []string{},
|
||||||
Children: []*model.Item{},
|
Children: []*model.Item{},
|
||||||
}
|
}
|
||||||
// Insert at beginning
|
|
||||||
m.orgFile.Items = append([]*model.Item{newItem}, m.orgFile.Items...)
|
// Check if we're in multi-file mode
|
||||||
m.setStatus("TODO captured!")
|
isMultiFile := len(m.orgFile.Items) > 0 && m.orgFile.Items[0].SourceFile != ""
|
||||||
|
|
||||||
|
if isMultiFile {
|
||||||
|
// In multi-file mode, add to the file of the highlighted item (using stored cursor position)
|
||||||
|
items := m.getVisibleItems()
|
||||||
|
targetFileItem := m.findTopLevelFileItem(items, m.captureCursor)
|
||||||
|
|
||||||
|
if targetFileItem != nil {
|
||||||
|
// Set the source file for the new item
|
||||||
|
newItem.SourceFile = targetFileItem.SourceFile
|
||||||
|
newItem.Level = 2 // Children of file items are level 2
|
||||||
|
|
||||||
|
// Insert at the beginning of the file item's children
|
||||||
|
targetFileItem.Children = append([]*model.Item{newItem}, targetFileItem.Children...)
|
||||||
|
targetFileItem.Folded = false // Unfold to show the new item
|
||||||
|
m.setStatus("TODO captured to " + targetFileItem.Title)
|
||||||
|
} else {
|
||||||
|
m.setStatus("Error: Could not find file to add to")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single file mode: insert at beginning
|
||||||
|
m.orgFile.Items = append([]*model.Item{newItem}, m.orgFile.Items...)
|
||||||
|
m.setStatus("TODO captured!")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m.mode = modeList
|
m.mode = modeList
|
||||||
m.textinput.Blur()
|
m.textinput.Blur()
|
||||||
m.cursor = 0
|
// Don't reset cursor, keep it where it was
|
||||||
return m, nil
|
return m, nil
|
||||||
case tea.KeyEsc:
|
case tea.KeyEsc:
|
||||||
m.mode = modeList
|
m.mode = modeList
|
||||||
|
|
@ -376,6 +411,49 @@ func (m uiModel) updateCapture(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findTopLevelFileItem finds the top-level file item that contains the item at the given cursor position
|
||||||
|
func (m *uiModel) findTopLevelFileItem(items []*model.Item, cursorPos int) *model.Item {
|
||||||
|
if cursorPos < 0 || cursorPos >= len(items) {
|
||||||
|
// Fallback to first file if cursor out of bounds
|
||||||
|
if len(m.orgFile.Items) > 0 {
|
||||||
|
return m.orgFile.Items[0]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedItem := items[cursorPos]
|
||||||
|
|
||||||
|
// Check if we're in multi-file mode
|
||||||
|
isMultiFile := len(m.orgFile.Items) > 0 && m.orgFile.Items[0].SourceFile != ""
|
||||||
|
|
||||||
|
if !isMultiFile {
|
||||||
|
// Not in multi-file mode, return nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the selected item itself is a file item (level 1 with SourceFile), return it
|
||||||
|
if selectedItem.SourceFile != "" && selectedItem.Level == 1 {
|
||||||
|
return selectedItem
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, find which top-level file item this item belongs to
|
||||||
|
// by checking the SourceFile field
|
||||||
|
if selectedItem.SourceFile != "" {
|
||||||
|
for _, fileItem := range m.orgFile.Items {
|
||||||
|
if fileItem.SourceFile == selectedItem.SourceFile {
|
||||||
|
return fileItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: return the first file item
|
||||||
|
if len(m.orgFile.Items) > 0 {
|
||||||
|
return m.orgFile.Items[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m uiModel) updateAddSubTask(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m uiModel) updateAddSubTask(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
|
|
||||||
|
|
@ -395,11 +473,12 @@ func (m uiModel) updateAddSubTask(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
// Create new sub-task
|
// Create new sub-task
|
||||||
newItem := &model.Item{
|
newItem := &model.Item{
|
||||||
Level: m.editingItem.Level + 1,
|
Level: m.editingItem.Level + 1,
|
||||||
State: defaultState,
|
State: defaultState,
|
||||||
Title: title,
|
Title: title,
|
||||||
Notes: []string{},
|
Notes: []string{},
|
||||||
Children: []*model.Item{},
|
Children: []*model.Item{},
|
||||||
|
SourceFile: m.editingItem.SourceFile, // Inherit source file from parent
|
||||||
}
|
}
|
||||||
m.editingItem.Children = append(m.editingItem.Children, newItem)
|
m.editingItem.Children = append(m.editingItem.Children, newItem)
|
||||||
m.editingItem.Folded = false // Unfold to show new sub-task
|
m.editingItem.Folded = false // Unfold to show new sub-task
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue